Я имел в виду этот вопрос и, поскольку я только что открыл для себя этот сайт, решил разместить его здесь.
Скажем, у меня есть таблица с меткой времени и состоянием для данного «объекта» (общее значение, а не объект ООП); есть ли оптимальный способ рассчитать время между состоянием и следующим появлением другого (или такого же) состояния (то, что я называю «поездкой») с помощью одного оператора SQL (внутренние операции SELECT и UNION не учитываются)?
Пример: В следующих случаях время поездки между Начальным и Готовым будет 6 дней, а между Начальным и Обзорным - 2 дня.
2008-08-01 13:30:00 - Initial
2008-08-02 13:30:00 - Work
2008-08-03 13:30:00 - Review
2008-08-04 13:30:00 - Work
2008-08-05 13:30:00 - Review
2008-08-06 13:30:00 - Accepted
2008-08-07 13:30:00 - Done
Не нужно быть универсальным, просто скажите, для какого SGBD ваше решение является специфическим, если не универсальным.


Я не думаю, что вы можете получить такой ответ с помощью одного оператора SQL, поскольку вы пытаетесь получить один результат из множества записей. Единственный способ добиться этого в SQL - получить поле временной метки для двух разных записей и вычислить разницу (dateiff). Следовательно, необходимы UNIONS или Inner Joins.
Я не уверен, что правильно понимаю вопрос, но вы можете сделать что-то вроде следующего, которое считывает таблицу за один проход, а затем использует производную таблицу для ее вычисления. Код SQL Server:
CREATE TABLE #testing
(
eventdatetime datetime NOT NULL,
state varchar(10) NOT NULL
)
INSERT INTO #testing (
eventdatetime,
state
)
SELECT '20080801 13:30:00', 'Initial' UNION ALL
SELECT '20080802 13:30:00', 'Work' UNION ALL
SELECT '20080803 13:30:00', 'Review' UNION ALL
SELECT '20080804 13:30:00', 'Work' UNION ALL
SELECT '20080805 13:30:00', 'Review' UNION ALL
SELECT '20080806 13:30:00', 'Accepted' UNION ALL
SELECT '20080807 13:30:00', 'Done'
SELECT DATEDIFF(dd, Initial, Review)
FROM (
SELECT MIN(CASE WHEN state='Initial' THEN eventdatetime END) AS Initial,
MIN(CASE WHEN state='Review' THEN eventdatetime END) AS Review
FROM #testing
) AS A
DROP TABLE #testing
create table A (
At datetime not null,
State varchar(20) not null
)
go
insert into A(At,State)
select '2008-08-01T13:30:00','Initial' union all
select '2008-08-02T13:30:00','Work' union all
select '2008-08-03T13:30:00','Review' union all
select '2008-08-04T13:30:00','Work' union all
select '2008-08-05T13:30:00','Review' union all
select '2008-08-06T13:30:00','Accepted' union all
select '2008-08-07T13:30:00','Done'
go
--Find trip time from Initial to Done
select DATEDIFF(day,t1.At,t2.At)
from
A t1
inner join
A t2
on
t1.State = 'Initial' and
t2.State = 'Review' and
t1.At < t2.At
left join
A t3
on
t3.State = 'Initial' and
t3.At > t1.At and
t4.At < t2.At
left join
A t4
on
t4.State = 'Review' and
t4.At < t2.At and
t4.At > t1.At
where
t3.At is null and
t4.At is null
Не сказал, разрешено ли присоединение или нет. Присоединения к t3 и t4 (и их сравнения) позволяют вам сказать, хотите ли вы самое раннее или самое позднее появление начального и конечного состояний (в этом случае я прошу последний «Начальный» и самый ранний «Обзор»)
В реальном коде мои начальное и конечное состояния были бы параметрами
Обновлено: К сожалению, необходимо включить «t3.At <t2.At» и «t4.At> t1.At», чтобы исправить некоторые нечетные последовательности состояний (например, если мы удалили второй «Обзор», а затем запросили из «Работа» "на" Просмотр ", исходный запрос не будет выполнен)
Вероятно, будет проще, если у вас есть порядковый номер, а также отметка времени: в большинстве СУБД вы можете создать столбец с автоматическим приращением и не изменять ни один из операторов INSERT. Затем вы присоединяетесь к таблице с ее копией, чтобы получить дельты
select after.moment - before.moment, before.state, after.state
from object_states before, object_states after
where after.sequence + 1 = before.sequence
(где детали синтаксиса SQL будут зависеть от системы базы данных).
-- Oracle SQl
CREATE TABLE ObjectState
(
startdate date NOT NULL,
state varchar2(10) NOT NULL
);
insert into ObjectState
select to_date('01-Aug-2008 13:30:00','dd-Mon-rrrr hh24:mi:ss'),'Initial' union all
select to_date('02-Aug-2008 13:30:00','dd-Mon-rrrr hh24:mi:ss'),'Work' union all
select to_date('03-Aug-2008 13:30:00','dd-Mon-rrrr hh24:mi:ss'),'Review' union all
select to_date('04-Aug-2008 13:30:00','dd-Mon-rrrr hh24:mi:ss'),'Work' union all
select to_date('05-Aug-2008 13:30:00','dd-Mon-rrrr hh24:mi:ss'),'Review' union all
select to_date('06-Aug-2008 13:30:00','dd-Mon-rrrr hh24:mi:ss'),'Accepted' union all
select to_date('07-Aug-2008 13:30:00','dd-Mon-rrrr hh24:mi:ss'),'Done';
-- Days in between two states
select o2.startdate - o1.startdate as days
from ObjectState o1, ObjectState o2
where o1.state = 'Initial'
and o2.state = 'Review';
Я думаю, что ваши шаги (каждую запись о вашей поездке можно рассматривать как шаг) можно где-то сгруппировать как часть одного и того же действия. Затем можно сгруппировать данные на нем, например, как:
SELECT Min(Tbl_Step.dateTimeStep) as tripBegin, _
Max(Tbl_Step.dateTimeStep) as tripEnd _
FROM
Tbl_Step
WHERE
id_Activity = 'AAAAAAA'
Используя этот принцип, вы можете затем вычислить другие агрегаты, такие как количество шагов в действии и т. д. Но вы не найдете способа SQL для вычисления таких значений, как разрыв между двумя шагами, поскольку такие данные не относятся ни к первому, ни ко второму шагу. Некоторые инструменты отчетности используют так называемые «текущие суммы» для вычисления таких промежуточных данных. В зависимости от ваших целей это может быть решением для вас.
Вот методология Oracle с использованием аналитической функции.
with data as (
SELECT 1 trip_id, to_date('20080801 13:30:00','YYYYMMDD HH24:mi:ss') dt, 'Initial' step from dual UNION ALL
SELECT 1 trip_id, to_date('20080802 13:30:00','YYYYMMDD HH24:mi:ss') dt, 'Work' step from dual UNION ALL
SELECT 1 trip_id, to_date('20080803 13:30:00','YYYYMMDD HH24:mi:ss') dt, 'Review' step from dual UNION ALL
SELECT 1 trip_id, to_date('20080804 13:30:00','YYYYMMDD HH24:mi:ss') dt, 'Work' step from dual UNION ALL
SELECT 1 trip_id, to_date('20080805 13:30:00','YYYYMMDD HH24:mi:ss') dt, 'Review' step from dual UNION ALL
SELECT 1 trip_id, to_date('20080806 13:30:00','YYYYMMDD HH24:mi:ss') dt, 'Accepted' step from dual UNION ALL
SELECT 1 trip_id, to_date('20080807 13:30:00','YYYYMMDD HH24:mi:ss') dt, 'Done' step from dual )
select trip_id,
step,
dt - lag(dt) over (partition by trip_id order by dt) trip_time
from data
/
1 Initial
1 Work 1
1 Review 1
1 Work 1
1 Review 1
1 Accepted 1
1 Done 1
Они очень часто используются в ситуациях, когда традиционно мы могли бы использовать самосоединение.
Синтаксис PostgreSQL:
DROP TABLE ObjectState;
CREATE TABLE ObjectState (
object_id integer not null,--foreign key
event_time timestamp NOT NULL,
state varchar(10) NOT NULL,
--Other fields
CONSTRAINT pk_ObjectState PRIMARY KEY (object_id,event_time)
);
Для данного состояния найти первое следующее состояние данного типа
select parent.object_id,parent.event_time,parent.state,min(child.event_time) as ch_event_time,min(child.event_time)-parent.event_time as step_time
from
ObjectState parent
join ObjectState child on (parent.object_id=child.object_id and parent.event_time<child.event_time)
where
--Starting state
parent.object_id=1 and parent.event_time=to_timestamp('01-Aug-2008 13:30:00','dd-Mon-yyyy hh24:mi:ss')
--needed state
and child.state='Review'
group by parent.object_id,parent.event_time,parent.state;
Этот запрос не самый короткий, но он должен быть легким для понимания и использоваться как часть других запросов:
Список событий и их продолжительность для данного объекта
select parent.object_id,parent.event_time,parent.state,min(child.event_time) as ch_event_time,
CASE WHEN parent.state<>'Done' and min(child.event_time) is null THEN (select localtimestamp)-parent.event_time ELSE min(child.event_time)-parent.event_time END as step_time
from
ObjectState parent
left outer join ObjectState child on (parent.object_id=child.object_id and parent.event_time<child.event_time)
where parent.object_id=4
group by parent.object_id,parent.event_time,parent.state
order by parent.object_id,parent.event_time,parent.state;
Список текущих состояний для объектов, которые еще не завершены.
select states.object_id,states.event_time,states.state,(select localtimestamp)-states.event_time as step_time
from
(select parent.object_id,parent.event_time,parent.state,min(child.event_time) as ch_event_time,min(child.event_time)-parent.event_time as step_time
from
ObjectState parent
left outer join ObjectState child on (parent.object_id=child.object_id and parent.event_time<child.event_time)
group by parent.object_id,parent.event_time,parent.state) states
where
states.object_id not in (select object_id from ObjectState where state='Done')
and ch_event_time is null;
Данные испытаний
insert into ObjectState (object_id,event_time,state)
select 1,to_timestamp('01-Aug-2008 13:30:00','dd-Mon-yyyy hh24:mi:ss'),'Initial' union all
select 1,to_timestamp('02-Aug-2008 13:40:00','dd-Mon-yyyy hh24:mi:ss'),'Work' union all
select 1,to_timestamp('03-Aug-2008 13:50:00','dd-Mon-yyyy hh24:mi:ss'),'Review' union all
select 1,to_timestamp('04-Aug-2008 14:30:00','dd-Mon-yyyy hh24:mi:ss'),'Work' union all
select 1,to_timestamp('04-Aug-2008 16:20:00','dd-Mon-yyyy hh24:mi:ss'),'Review' union all
select 1,to_timestamp('06-Aug-2008 18:00:00','dd-Mon-yyyy hh24:mi:ss'),'Accepted' union all
select 1,to_timestamp('07-Aug-2008 21:30:00','dd-Mon-yyyy hh24:mi:ss'),'Done';
insert into ObjectState (object_id,event_time,state)
select 2,to_timestamp('01-Aug-2008 13:30:00','dd-Mon-yyyy hh24:mi:ss'),'Initial' union all
select 2,to_timestamp('02-Aug-2008 13:40:00','dd-Mon-yyyy hh24:mi:ss'),'Work' union all
select 2,to_timestamp('07-Aug-2008 13:50:00','dd-Mon-yyyy hh24:mi:ss'),'Review' union all
select 2,to_timestamp('14-Aug-2008 14:30:00','dd-Mon-yyyy hh24:mi:ss'),'Work' union all
select 2,to_timestamp('15-Aug-2008 16:20:00','dd-Mon-yyyy hh24:mi:ss'),'Review' union all
select 2,to_timestamp('16-Aug-2008 18:02:00','dd-Mon-yyyy hh24:mi:ss'),'Accepted' union all
select 2,to_timestamp('17-Aug-2008 22:10:00','dd-Mon-yyyy hh24:mi:ss'),'Done';
insert into ObjectState (object_id,event_time,state)
select 3,to_timestamp('12-Sep-2008 13:30:00','dd-Mon-yyyy hh24:mi:ss'),'Initial' union all
select 3,to_timestamp('13-Sep-2008 13:40:00','dd-Mon-yyyy hh24:mi:ss'),'Work' union all
select 3,to_timestamp('14-Sep-2008 13:50:00','dd-Mon-yyyy hh24:mi:ss'),'Review' union all
select 3,to_timestamp('15-Sep-2008 14:30:00','dd-Mon-yyyy hh24:mi:ss'),'Work' union all
select 3,to_timestamp('16-Sep-2008 16:20:00','dd-Mon-yyyy hh24:mi:ss'),'Review';
insert into ObjectState (object_id,event_time,state)
select 4,to_timestamp('21-Aug-2008 03:10:00','dd-Mon-yyyy hh24:mi:ss'),'Initial' union all
select 4,to_timestamp('22-Aug-2008 03:40:00','dd-Mon-yyyy hh24:mi:ss'),'Work' union all
select 4,to_timestamp('23-Aug-2008 03:20:00','dd-Mon-yyyy hh24:mi:ss'),'Review' union all
select 4,to_timestamp('24-Aug-2008 04:30:00','dd-Mon-yyyy hh24:mi:ss'),'Work';
Я пытался сделать это в MySQL. Вам нужно будет использовать переменную, поскольку в MySQL нет функции ранжирования, поэтому она будет выглядеть так:
set @trip1 = 0; set @trip2 = 0;
SELECT trip1.`date` as startdate, datediff(trip2.`date`, trip1.`date`) length_of_trip
FROM
(SELECT @trip1 := @trip1 + 1 as rank1, `date` from trip where state='Initial') as trip1
INNER JOIN
(SELECT @trip2 := @trip2 + 1 as rank2, `date` from trip where state='Done') as trip2
ON rank1 = rank2;
Я предполагаю, что вы хотите рассчитать время между состояниями «Исходное» и «Готово».
+---------------------+----------------+
| startdate | length_of_trip |
+---------------------+----------------+
| 2008-08-01 13:30:00 | 6 |
+---------------------+----------------+
Ладно, это немного не по зубам, но я создал веб-приложение для отслеживания схваток моей жены незадолго до того, как у нас родился ребенок, чтобы я мог видеть с работы, когда приближается время для того, чтобы ехать в больницу. Во всяком случае, я довольно легко построил эту базовую вещь в виде двух представлений.
create table contractions time_date timestamp primary key;
create view contraction_time as
SELECT a.time_date, max(b.prev_time) AS prev_time
FROM contractions a, ( SELECT contractions.time_date AS prev_time
FROM contractions) b
WHERE b.prev_time < a.time_date
GROUP BY a.time_date;
create view time_between as
SELECT contraction_time.time_date, contraction_time.prev_time, contraction_time.time_date - contraction_time.prev_time
FROM contraction_time;
Очевидно, это можно было бы сделать как подвыборку, но я использовал промежуточные представления и для других вещей, и это сработало.