Process distinct comma-separated value in oracle - sql

I have a column with the following data:
Brand
-------------
Audi, Opel, Ford
Skoda, Renault
Audi, BMW
Audi, Volkswagen, Opel
Toyota, Hyundai
I would like to have query which automates assign the data into group as following:
Brand
-------------------
Audi, Opel, Ford, BMW, Volkwagen
Skoda, Renault
Toyota, Hyundai
Note that if we insert another record into the table like this ...
Toyota, BMW
... the required output would be:
Brand
-------------------
Audi, Opel, Ford, BMW, Volkwagen, Toyota, Hyundai
Skoda, Renault

This is an interesting and difficult problem, obscured by your poor data model (which violates First Normal Form). Normalizing the data - and de-normalizing at the end - is trivial, it's just an annoyance (and it will make the query much slower). The interesting part: the input groups are the nodes of a graph, two nodes are connected if they have a "make" in common. You need to find the connected components of the graph; this is the interesting problem.
Here is a complete solution (creating the testing data on the fly, in the first factored subquery in the with clause). Question for you though: even assuming that this solution works for you and you put it in production, who is going to maintain it in the future?
EDIT It occurred to me that my original query can be simplified. Here is the revised version; you can click on the Edited link below the answer if you are curious to see the original version.
with
sample_data (brand) as (
select 'Audi, Opel, Ford' from dual union all
select 'Skoda, Renault' from dual union all
select 'Audi, BMW' from dual union all
select 'Audi, Volkswagen, Opel' from dual union all
select 'Toyota, Hyundai' from dual union all
select 'Tesla' from dual
)
, prep (id, brand) as (
select rownum, brand
from sample_data
)
, fnf (id, brand) as (
select p.id, ca.brand
from prep p cross apply
( select trim(regexp_substr(p.brand, '[^,]+', 1, level)) as brand
from dual
connect by level <= regexp_count(p.brand, '[^,]+')
) ca
)
, g (b1, b2) as (
select distinct fnf1.brand, fnf2.brand
from fnf fnf1 join fnf fnf2 on fnf1.id = fnf2.id
)
, cc (rt, brand) as (
select min(connect_by_root b1), b2
from g
connect by nocycle b1 = prior b2
group by b2
)
select listagg(brand, ', ') within group (order by null) as brand
from cc
group by rt;
Output:
BRAND
---------------------------------------------
Audi, BMW, Ford, Opel, Volkswagen
Hyundai, Toyota
Renault, Skoda
Tesla

That is standard Connected components problem. You can find fast pl/sql solution for production use here: http://orasql.org/2017/09/29/connected-components/
Or in case of just educational purposes, you can use SQL-only solution:
https://gist.github.com/xtender/b6e5cac4dec461c0121145b0e62c5cf5
with t(Brand) as (
select 'Audi, Opel, Ford' brand from dual union all
select 'Skoda, Renault' from dual union all
select 'Audi, BMW' from dual union all
select 'Audi, Volkswagen, Opel' from dual union all
select 'Toyota, Hyundai' from dual union all
select 'Tesla' from dual union all
select 'A'||level||', A'||(level+1) from dual connect by level<=500 union all
select 'B'||level||', B'||(level+1) from dual connect by level<=500 union all
select 'C'||level||', C'||(level+1) from dual connect by level<=500
)
,split_tab as (
select
dense_rank()over(order by t.brand) rn
,x.*
from t,
xmltable(
'ora:tokenize(concat(",",.),",")[position()>1]'
passing t.brand
columns
n for ordinality
,name varchar2(20) path 'normalize-space(.)'
) x
)
,pairs as (
select
t1.rn, t1.name name1, t2.name name2
from split_tab t1
,split_tab t2
where t1.rn=t2.rn
)
select listagg(x,',')within group(order by x)
from (
select x, min(root) grp
from (
select distinct connect_by_root(name1) root, name1 x
from pairs
connect by nocycle
prior name1 = name2
)
group by x
)
group by grp
/
PS. I've split my solution into smallest possible steps, so you can check each CTE separately step-by-step to view how to get results.

Related

Pivot with dynamic columns in oracle 11g

I have 3 tables (Potions and Reagents and relation of both) and I want to pivot the results, pass to explain:
TABLE POTIONS
ID DESCRIPTION
1 Heal
2 Mana
3 Explosion
TABLE REAGENTS
ID DESCRIPTION
1 Base
2 Mandrake
3 Tulip
4 Sunflower
5 Powder
TABLE POTION_REAGENTS
ID_POTION ID_REAGENT
1 1
1 3
2 1
2 2
2 5
3 4
3 5
I want to obtain the result like this but I dont know how pivot with dynamic columns
POTION REAG_1 REAG_2 REAG_3
Heal Base Tulip NULL
Mana Base Mandrake Powder
Explosion Sunflower Powder NULL
I want if new potion have 4 Reagent the select return's REAG_4 column. Its possible?
Thx for your time!
You cannot use a dynamic pivot; you must have a pre-defined set of values in the PIVOT clause. However, you can use the ROW_NUMBER analytic function to pre-generate a numeric index for potion reagents:
SELECT potion,
reagent1,
reagent2,
reagent3,
reagent4
FROM (
SELECT p.id,
p.description AS potion,
r.description AS reagent,
ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY r.description) AS rn
FROM potions p
INNER JOIN potion_reagents pr
ON (p.id = pr.id_potion)
INNER JOIN reagents r
ON (pr.id_reagent = r.id)
)
PIVOT (
MAX(reagent) FOR rn IN (1 AS reagent1, 2 AS reagent2, 3 AS reagent3, 4 AS reagent4)
)
Which, for your sample data:
CREATE TABLE POTIONS (ID, DESCRIPTION) AS
SELECT 1, 'Heal' FROM DUAL UNION ALL
SELECT 2, 'Mana' FROM DUAL UNION ALL
SELECT 3, 'Explosion' FROM DUAL;
CREATE TABLE REAGENTS (ID, DESCRIPTION) AS
SELECT 1, 'Base' FROM DUAL UNION ALL
SELECT 2, 'Mandrake' FROM DUAL UNION ALL
SELECT 3, 'Tulip' FROM DUAL UNION ALL
SELECT 4, 'Sunflower' FROM DUAL UNION ALL
SELECT 5, 'Powder' FROM DUAL;
CREATE TABLE POTION_REAGENTS (ID_POTION, ID_REAGENT) AS
SELECT 1, 1 FROM DUAL UNION ALL
SELECT 1, 3 FROM DUAL UNION ALL
SELECT 2, 1 FROM DUAL UNION ALL
SELECT 2, 2 FROM DUAL UNION ALL
SELECT 2, 5 FROM DUAL UNION ALL
SELECT 3, 4 FROM DUAL UNION ALL
SELECT 3, 5 FROM DUAL;
Outputs:
POTION
REAGENT1
REAGENT2
REAGENT3
REAGENT4
Explosion
Powder
Sunflower
null
null
Heal
Base
Tulip
null
null
Mana
Base
Mandrake
Powder
null
fiddle
To me it seems you're a making a game or something of a kind. That means there is another "external" programming is being used to process data you're getting from the database.
For that case there is a possibility to obtain a pivot result with dynamical set of columns in XML format. You need the "pivot xml"
select *
from (select p.description p_desc,
r.description r_desc,
'REAG_' || row_number() over (partition by p.description order by r.description) reag_num
from potion_reagents pr
join reagents r
on r.id = pr.id_reagent
join potions p
on p.id = pr.id_potion)
pivot xml(
min(r_desc)
for reag_num in (ANY)
)
As I said, the answer you'll get will be in XML format which is quite simple to parse. Here is an example for "Mana" potion with a fourth reagent I named "Secret"
<PivotSet>
<item>
<column name = "REAG_NUM">REAG_1</column>
<column name = "MIN(R_DESC)">Base</column>
</item>
<item>
<column name = "REAG_NUM">REAG_2</column>
<column name = "MIN(R_DESC)">Mandrake</column>
</item>
<item>
<column name = "REAG_NUM">REAG_3</column>
<column name = "MIN(R_DESC)">Powder</column>
</item>
<item>
<column name = "REAG_NUM">REAG_4</column>
<column name = "MIN(R_DESC)">Secret</column>
</item>
</PivotSet>
Here's a dbfiddle with an example based on data you've provide us with

Sort strings/words alphabetically separated by comma within a column in SQL (entire column)

Lets say that I have a table the following data:
(there are a 1000+ more rows like this)
Bird
----------------------------
Sparrow, Eagle, Crow
Woodpecker, Sparrow
Crow, Eagle
etc. etc.
I want the final column to be sorted out alphabetically. Something like this:
Bird
--------------------
Crow, Eagle, Sparrow
Sparrow, Woodpecker
Crow, Eagle
etc. etc.
Need to know a SQL query that can do that. Possibly SQL Developer.
Here is an Oracle solution using Common Table Expressions (CTEs) to break the problem down. Not sure if this will help, but maybe it will give you an idea or a starting point that you can apply to your environment.
SQL> -- Set up original data set
SQL> with bird_tbl(id, unsorted_list) as (
select 1, 'Sparrow, Eagle, Crow' from dual union all
select 2, 'Woodpecker, Sparrow' from dual union all
select 3, 'Crow, Eagle' from dual
),
-- Split the list into a row for each element
split_tbl(id, bird) as (
select id, regexp_substr(unsorted_list, '(.*?)(, |$)', 1, level, null, 1)
from bird_tbl
connect by level <= regexp_count(unsorted_list, ', ')+1
and prior id = id
and prior sys_guid() is not null
)
-- select * from split_tbl;
-- Rebuild the sorted row
select id, listagg(bird, ', ')
within group (order by bird) sorted_list
from split_tbl
group by id;
ID SORTED_LIST
---------- --------------------
1 Crow, Eagle, Sparrow
2 Sparrow, Woodpecker
3 Crow, Eagle
EDIT: Here's how to apply to your situation. Just replace <your_primary_key> with the primary key column name, <your_column_name> with the name of the column that contains the unsorted list and <your_table_name> with the name of the table.
with split_tbl(<your_primary_key>, <your_column_name>) as (
select <your_primary_key>, regexp_substr(<your_column_name>, '(.*?)(, |$)', 1, level, null, 1)
from <your_table_name>
connect by level <= regexp_count(<your_column_name>, ', ')+1
and prior <your_primary_key> = <your_primary_key>
and prior sys_guid() is not null
)
-- select * from split_tbl;
-- Rebuild the sorted row
select <your_primary_key>, listagg(<your_column_name>, ', ')
within group (order by <your_column_name>) sorted_list
from split_tbl
group by <your_primary_key>;

find the missing values from a set of values, using SQL

How can I find a missing values from a set of values, using SQL (Oracle DB)
e.g.
SELECT NAME
FROM ACCOUNT
WHERE ACCOUNT.NAME IN ('FORD','HYUNDAI','TOYOTA','BMW'...)
(The "IN" clause may contain hundreds of values)
If 'HYUNDAI' is missing in the ACCOUNT table, I need to get the result as "HYUNDAI".
Currently I use the result of the above query to do a Vlookup against the original set of values to find the missing values, I want to directly get the missing values without doing the Vlookup.
Thanks
Kiran,
You got it reversed. Do this: http://www.sqlfiddle.com/#!2/09239/3
SELECT Brand
FROM
(
-- Oracle can't make a row without a table, need to use DUAL dummy table
select 'FORD' as Brand from dual union
select 'HYUNDAI' from dual union
select 'TOYOTA' fom dual union
select 'BMW' from dual
) x
where Brand not in (select BrandName from account)
Sample Account data:
create table account(AccountId int, BrandName varchar(10));
insert into account(AccountId, BrandName) values
(1,'FORD'),
(2,'TOYOTA'),
(3,'BMW');
Output:
| BRAND |
-----------
| HYUNDAI |
Better yet, materialized the brands to a table:
select *
from Brand
where BrandName not in (select BrandName from account)
Output:
| BRANDNAME |
-------------
| HYUNDAI |
Sample data and live test: http://www.sqlfiddle.com/#!2/09239/1
CREATE TABLE Brand
(`BrandName` varchar(7));
INSERT INTO Brand
(`BrandName`)
VALUES
('FORD'),
('HYUNDAI'),
('TOYOTA'),
('BMW');
create table account(AccountId int, BrandName varchar(10));
insert into account(AccountId, BrandName) values
(1,'FORD'),
(2,'TOYOTA'),
(3,'BMW');
You should use Except: EXCEPT returns any distinct values from the left query that are not also found on the right query.
WITH SomeRows(datacol) --It will look for missing stuff here
AS( SELECT *
FROM ( VALUES ('FORD'),
('TOYOTA'),
('BMW')
) AS F (datacol)),
AllRows (datacol) --This has everthing
AS( SELECT *
FROM ( VALUES ('FORD'),
('HYUNDAI'),
('TOYOTA'),
('BMW')
) AS F (datacol))
SELECT datacol
FROM AllRows
EXCEPT
SELECT datacol
FROM SomeRows
You can do:
SELECT a.val
FROM
(
SELECT 'FORD' val UNION ALL
SELECT 'HYUNDAI' UNION ALL
SELECT 'TOYOTA' UNION ALL
SELECT 'BMW' UNION ALL
etc...
etc...
) a
LEFT JOIN account b ON a.val = b.name
WHERE b.name IS NULL
This worked perfectly, thanks Michael.
SELECT Brand
FROM
( -- Oracle can't make a row without a table, need to use DUAL dummy table
select 'FORD' as Brand from dual union
select 'HYUNDAI' from dual union
select 'TOYOTA' fom dual union
select 'BMW' from dual
)
where Brand not in (select BrandName from account)
Luxspes and Zane thank you for your inputs
Contributing Excel code to make the typing of the answer easier:
Say column A has the values (Ford, Hyundai,...).
In column B, put this in every cell:
select 'x' as brand from dual union
In column C, write this formula, and copy it down.
=REPLACE(A2,9,1,A1)
All of the select/union statements should appear in column C.

create view for items not in list

I have two tables. Table 1 is a master list of equipment with equipment_id and equipment_description. So, let's say for this table I have ten equipment_id's. 1,2,3....10 each with some description attached.
Table 2 logs when the equipment has been inspected:
equipment_id|inspection_date
1 | '1-22-2012'
2 | '1-22-2012'
4 | '1-22-2012'
2 | '1-23-2012'
3 | '1-23-2012'
I've created a view, v_dates which pulls out of table 2 all of the distinct inspection dates - not sure if I needed it but did it anyway.
I would like to create another view which shows all equipment that was NOT inspected for each date in the v_dates. So it would show:
3 | '1-22-2012'
5 | '1-22-2012'
and so on.
Rookie here and just not sure how to join these tables correctly. Can't get it to work and would appreciate any help.
Untested, but I think this should give the desired result:
SELECT i.id,d.date FROM
( SELECT DISTINCT inspection_date AS date FROM inspections ORDER BY 1 ) d
LEFT JOIN
inspections i
ON d.date=i.date
WHERE i.date IS NULL
GROUP BY 1,2
ORDER BY 1,2
Like mentioned in the comments would a table with inspection dates really help.
The following appears to work based on my test data using SQL SERVER 2005. I am using a CROSS JOIN of distinct dates along with a LEFT JOIN to throw out EQUIPMENT_ID records that exist for those dates.
Sorry, I am having problems getting my code formatting correct with tabs and spaces...
IF OBJECT_ID('tempdb..#EQUIPMENT') IS NOT NULL
DROP TABLE #EQUIPMENT
CREATE TABLE #EQUIPMENT
( EQUIPMENT_ID smallint,
EQUIPMENT_DESC varchar(32)
)
INSERT INTO #EQUIPMENT
( EQUIPMENT_ID, EQUIPMENT_DESC )
SELECT 1, 'AAA'
UNION SELECT 2, 'BBB'
UNION SELECT 3, 'CCC'
UNION SELECT 4, 'DDD'
UNION SELECT 5, 'EEE'
UNION SELECT 6, 'FFF'
UNION SELECT 7, 'GGG'
UNION SELECT 8, 'HHH'
UNION SELECT 9, 'III'
UNION SELECT 10, 'JJJ'
IF OBJECT_ID('tempdb..#INSPECTION') IS NOT NULL
DROP TABLE #INSPECTION
CREATE TABLE #INSPECTION
( EQUIPMENT_ID smallint,
INSPECTION_DATE smalldatetime
)
INSERT INTO #INSPECTION
( EQUIPMENT_ID, INSPECTION_DATE )
SELECT 1, '1-22-2012'
UNION SELECT 1, '1-27-2012'
UNION SELECT 3, '1-27-2012'
UNION SELECT 5, '1-29-2012'
UNION SELECT 7, '1-22-2012'
UNION SELECT 7, '1-27-2012'
UNION SELECT 7, '1-29-2012'
SELECT E.EQUIPMENT_ID, D.INSPECTION_DATE
FROM #EQUIPMENT E
CROSS JOIN ( SELECT DISTINCT INSPECTION_DATE
FROM #INSPECTION
) D
LEFT JOIN #INSPECTION I2
ON E.EQUIPMENT_ID = I2.EQUIPMENT_ID
AND D.INSPECTION_DATE = I2.INSPECTION_DATE
WHERE I2.EQUIPMENT_ID IS NULL
ORDER BY E.EQUIPMENT_ID, D.INSPECTION_DATE
As per my comment to the question, you really need a table of valid inspection dates. It makes the sql much more sensible, and besides it's the only way to do it if you want to see all items listed for dates when inspections were supposed to be done, but no inspections were done.
So, assuming the two tables:
create table inspections (equipment_id int, inspection_date date);
create table inspection_dates (id int, inspection_date date);
Then a join to get all the equipment that does not have an inspection on a date when an inspection should have taken place would be:
select i.equipment_id, id.inspection_date
from inspection_dates id,
(select distinct equipment_id from inspections) i
where not exists (select * from inspections i2
where i2.inspection_date = id.inspection_date
and i2.equipment_id = i.equipment_id);
You want the combos that do not exist. Thus the not exists predicate.
Note again, that presumably you would have a table for all the unique equipment_ids, but not knowing that I had to construct it myself in place.

Trip time calculation in relational databases?

I had this question in mind and since I just discovered this site I decided to post it here.
Let's say I have a table with a timestamp and a state for a given "object" (generic meaning, not OOP object); is there an optimal way to calculate the time between a state and the next occurrence of another (or same) state (what I call a "trip") with a single SQL statement (inner SELECTs and UNIONs aren't counted)?
Ex: For the following, the trip time between Initial and Done would be 6 days, but between Initial and Review it would be 2 days.
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
No need to be generic, just say what SGBD your solution is specific to if not generic.
Here's an Oracle methodology using an analytic function.
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
These are very commonly used in situations where traditionally we might use a self-join.
PostgreSQL syntax :
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)
);
For given state find first folowing state of given type
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;
This query is not the shortest posible but it should be easy to understand and used as part of other queries :
List events and their duration for given object
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;
List current states for objects that are not "done"
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;
Test data
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';
I don't think you can get that answer with one SQL statement as you are trying to obtain one result from many records. The only way to achieve that in SQL is to get the timestamp field for two different records and calculate the difference (datediff). Therefore, UNIONS or Inner Joins are needed.
I'm not sure I understand the question exactly, but you can do something like the following which reads the table in one pass then uses a derived table to calculate it. SQL Server code:
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
It is probably easier if you have a sequence number as well as the time-stamp: in most RDBMSs you can create an auto-increment column and not change any of the INSERT statements. Then you join the table with a copy of itself to get the deltas
select after.moment - before.moment, before.state, after.state
from object_states before, object_states after
where after.sequence + 1 = before.sequence
(where the details of SQL syntax will vary according to which database system).
-- 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';
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
Didn't say whether joins were allowed or not. Joins to t3 and t4 (and their comparisons) let you say whether you want the earliest or latest occurrence of the start and end states (in this case, I'm asking for latest "Initial" and earliest "Review")
In real code, my start and end states would be parameters
Edit: Oops, need to include "t3.At < t2.At" and "t4.At > t1.At", to fix some odd sequences of States (e.g. If we removed the second "Review" and then queried from "Work" to "Review", the original query will fail)
I think that your steps (each record of your trip can be seen as a step) can be somewhere grouped together as part of the same activity. It is then possible to group your data on it, as, for example:
SELECT Min(Tbl_Step.dateTimeStep) as tripBegin, _
Max(Tbl_Step.dateTimeStep) as tripEnd _
FROM
Tbl_Step
WHERE
id_Activity = 'AAAAAAA'
Using this principle, you can then calculate other aggregates like the number of steps in the activity and so on. But you will not find an SQL way to calculate values like gap between 2 steps, as such a data does not belong either to the first or to the second step. Some reporting tools use what they call "running sums" to calculate such intermediate data. Depending on your objectives, this might be a solution for you.
I tried to do this in MySQL. You would need to use a variable since there is no rank function in MySQL, so it would go like this:
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;
I am assuming that you want to calculate the time between 'Initial' and 'Done' states.
+---------------------+----------------+
| startdate | length_of_trip |
+---------------------+----------------+
| 2008-08-01 13:30:00 | 6 |
+---------------------+----------------+
Ok, this is a bit beyond geeky, but I built a web application to track my wife's contractions just before we had a baby so that I could see from work when it was getting close to time to go to the hospital. Anyway, I built this basic thing fairly easily as two views.
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;
This could be done as a subselect obviously as well, but I used the intermediate views for other things as well, and so this worked out well.