Results 1 to 13 of 13
  1. #1
    Join Date
    Feb 2013
    Posts
    4

    Question Unanswered: Grouping Consecutive Rows in a Table

    SQL Server 2008.

    From individual event logs I have generated a table where arrivals and departures at a location are registered per device. As there are multiple registration points, there might be multiple consecutive registrations per location.
    If this is the case I need to filter those out and have one registration per location and in the result I need to get the earliest arrival and the latest departure of these consecutive rows.

    Part of the table:

    logID deviceID Arrived Departed LocationID Grp1 Grp2 Grp
    34854 4108 2013-02-07 17:51:05.000 2013-02-07 17:51:15.000 5 1 1 0
    34920 4108 2013-02-07 17:51:15.000 2013-02-07 17:51:26.000 5 2 2 0
    35002 4108 2013-02-07 17:51:27.000 2013-02-07 17:51:37.000 3 3 1 2
    35089 4108 2013-02-07 17:51:41.000 2013-02-07 17:51:54.000 4 4 1 3
    35173 4108 2013-02-07 17:51:54.000 2013-02-07 17:52:05.000 4 5 2 3
    35265 4108 2013-02-07 17:52:08.000 2013-02-07 17:52:19.000 2 6 1 5
    35335 4108 2013-02-07 17:52:19.000 2013-02-07 17:52:36.000 3 7 2 5
    35452 4108 2013-02-07 17:52:37.000 2013-02-07 17:52:49.000 2 8 2 6
    35521 4108 2013-02-07 17:52:49.000 2013-02-07 17:53:06.000 3 9 3 6
    35635 4108 2013-02-07 17:53:07.000 2013-02-07 17:53:20.000 4 10 3 7
    35717 4108 2013-02-07 17:53:20.000 2013-02-07 17:53:30.000 4 11 4 7
    35800 4108 2013-02-07 17:53:32.000 2013-02-07 17:53:42.000 5 12 3 9
    35867 4108 2013-02-07 17:53:43.000 2013-02-07 17:53:53.000 6 13 1 12
    35960 4108 2013-02-07 17:53:57.000 2013-02-07 17:54:10.000 3 14 4 10
    36058 4108 2013-02-07 17:54:12.000 2013-02-07 17:54:25.000 5 15 4 11
    36159 4108 2013-02-07 17:54:28.000 2013-02-07 17:54:38.000 5 16 5 11
    36243 4108 2013-02-07 17:54:41.000 2013-02-07 17:54:51.000 5 17 6 11
    36329 4108 2013-02-07 17:54:56.000 2013-02-07 17:55:09.000 2 18 3 15
    36433 4108 2013-02-07 17:55:11.000 2013-02-07 17:55:24.000 2 19 4 15
    36525 4108 2013-02-07 17:55:26.000 2013-02-07 17:55:39.000 4 20 5 15
    36622 4108 2013-02-07 17:55:40.000 2013-02-07 17:55:53.000 3 21 5 16
    36717 4108 2013-02-07 17:55:56.000 2013-02-07 17:56:09.000 4 22 6 16
    36808 4108 2013-02-07 17:56:10.000 2013-02-07 17:56:23.000 4 23 7 16
    36905 4108 2013-02-07 17:56:24.000 2013-02-07 17:56:37.000 2 24 5 19
    37004 4108 2013-02-07 17:56:39.000 2013-02-07 17:56:52.000 2 25 6 19
    37106 4108 2013-02-07 17:56:57.000 2013-02-07 17:57:10.000 5 26 7 19
    37199 4108 2013-02-07 17:57:11.000 2013-02-07 17:57:21.000 2 27 7 20
    37271 4108 2013-02-07 17:57:21.000 2013-02-07 17:57:31.000 5 28 8 20
    37339 4108 2013-02-07 17:57:32.000 2013-02-07 17:57:42.000 2 29 8 21
    37412 4108 2013-02-07 17:57:44.000 2013-02-07 17:57:54.000 6 30 2 28


    So as long the field LocationID is the same in the next row, it needs to be grouped.

    I have added the rows Grp1, Grp2, Grp in an attempt to get an unique grouping number with the following script in the select statement:

    ,ROW_NUMBER() OVER(PARTITION BY DeviceID
    ORDER BY logID) AS Grp1
    ,ROW_NUMBER() OVER(PARTITION BY DeviceID, LocationID
    ORDER BY logID) AS Grp2
    ,ROW_NUMBER() OVER(PARTITION BY DeviceID
    ORDER BY logID)
    -
    ROW_NUMBER() OVER(PARTITION BY DeviceID, LocationID
    ORDER BY logID) AS Grp

    By subtracting Grp2 from Grp1 (Grp = Grp1 - Grp2) I hoped to get an unique group number for each set of equal consecutive locations, however the Grp2 column does not restart from 1 each time the LocationID changes: Grp2 in line 7 should have been 1 again, but it is 2 because this is the second row with LocationID = 3 in the list.

    I do not know if using ROW_NUMBER is the way to go. I hope some of you have an idea how to tackle this one. It needs to be an effective solution as we easily talk about a table of a few million records....

    Thanks in advance.

  2. #2
    Join Date
    Nov 2004
    Posts
    1,427
    Provided Answers: 4
    Hey Tom,

    I'm not sure if this is what you are after. It helps to give the raw data and the result you expect.

    Code:
    SELECT deviceID, LocationID, MIN(Arrived) as minArrived, MAX(Departed) as MaxDeparted
    FROM DaTable
    GROUP BY deviceID, LocationID
    
    deviceID	LocationID	minArrived		MaxDeparted
    4108		2		2013-02-07 17:52:08.000	2013-02-07 17:57:42.000
    4108		3		2013-02-07 17:51:27.000	2013-02-07 17:55:53.000
    4108		4		2013-02-07 17:51:41.000	2013-02-07 17:56:23.000
    4108		5		2013-02-07 17:51:05.000	2013-02-07 17:57:31.000
    4108		6		2013-02-07 17:53:43.000	2013-02-07 17:57:54.000
    With kind regards . . . . . SQL Server 2000/2005/2012
    Wim

    Grabel's Law: 2 is not equal to 3 -- not even for very large values of 2.
    Pat Phelan's Law: 2 very definitely CAN equal 3 -- in at least two programming languages

  3. #3
    Join Date
    Feb 2013
    Posts
    4
    Hi Wim,

    Thank you for your input. I'm new to this forum and bear with me if I do not know how to best provide the raw data.

    I believe I managed to create the table creation code:

    Code:
    IF OBJECT_ID('dbo.TST_Location', 'U') IS NOT NULL
      DROP TABLE dbo.TST_Location;
    GO
    
    CREATE TABLE dbo.TST_Location
    (
    	[logID] [bigint] NOT NULL,
    	[deviceID] [int] NOT NULL,
    	[Arrived] [datetime] NOT NULL,
    	[Departed] [datetime] NOT NULL,
    	[LocationID] [int] NOT NULL
    ) ON [PRIMARY];
    GO
    
    INSERT INTO dbo.TST_Location VALUES(34854, 4108, '20130207 17:51:05.000', '20130207 17:51:15.000', 5);
    INSERT INTO dbo.TST_Location VALUES(34920, 4108, '20130207 17:51:15.000', '20130207 17:51:26.000', 5);
    INSERT INTO dbo.TST_Location VALUES(35002, 4108, '20130207 17:51:27.000', '20130207 17:51:37.000', 3);
    INSERT INTO dbo.TST_Location VALUES(35089, 4108, '20130207 17:51:41.000', '20130207 17:51:54.000', 4);
    INSERT INTO dbo.TST_Location VALUES(35173, 4108, '20130207 17:51:54.000', '20130207 17:52:05.000', 4);
    INSERT INTO dbo.TST_Location VALUES(35265, 4108, '20130207 17:52:08.000', '20130207 17:52:19.000', 2);
    INSERT INTO dbo.TST_Location VALUES(35335, 4108, '20130207 17:52:19.000', '20130207 17:52:36.000', 3);
    INSERT INTO dbo.TST_Location VALUES(35452, 4108, '20130207 17:52:37.000', '20130207 17:52:49.000', 2);
    INSERT INTO dbo.TST_Location VALUES(35521, 4108, '20130207 17:52:49.000', '20130207 17:53:06.000', 3);
    INSERT INTO dbo.TST_Location VALUES(35635, 4108, '20130207 17:53:07.000', '20130207 17:53:20.000', 4);
    INSERT INTO dbo.TST_Location VALUES(35717, 4108, '20130207 17:53:20.000', '20130207 17:53:30.000', 4);
    INSERT INTO dbo.TST_Location VALUES(35800, 4108, '20130207 17:53:32.000', '20130207 17:53:42.000', 5);
    INSERT INTO dbo.TST_Location VALUES(35867, 4108, '20130207 17:53:43.000', '20130207 17:53:53.000', 6);
    INSERT INTO dbo.TST_Location VALUES(35960, 4108, '20130207 17:53:57.000', '20130207 17:54:10.000', 3);
    INSERT INTO dbo.TST_Location VALUES(36058, 4108, '20130207 17:54:12.000', '20130207 17:54:25.000', 5);
    INSERT INTO dbo.TST_Location VALUES(36159, 4108, '20130207 17:54:28.000', '20130207 17:54:38.000', 5);
    INSERT INTO dbo.TST_Location VALUES(36243, 4108, '20130207 17:54:41.000', '20130207 17:54:51.000', 5);
    INSERT INTO dbo.TST_Location VALUES(36329, 4108, '20130207 17:54:56.000', '20130207 17:55:09.000', 2);
    INSERT INTO dbo.TST_Location VALUES(36433, 4108, '20130207 17:55:11.000', '20130207 17:55:24.000', 2);
    INSERT INTO dbo.TST_Location VALUES(36525, 4108, '20130207 17:55:26.000', '20130207 17:55:39.000', 4);
    INSERT INTO dbo.TST_Location VALUES(36622, 4108, '20130207 17:55:40.000', '20130207 17:55:53.000', 3);
    INSERT INTO dbo.TST_Location VALUES(36717, 4108, '20130207 17:55:56.000', '20130207 17:56:09.000', 4);
    INSERT INTO dbo.TST_Location VALUES(36808, 4108, '20130207 17:56:10.000', '20130207 17:56:23.000', 4);
    INSERT INTO dbo.TST_Location VALUES(36905, 4108, '20130207 17:56:24.000', '20130207 17:56:37.000', 2);
    INSERT INTO dbo.TST_Location VALUES(37004, 4108, '20130207 17:56:39.000', '20130207 17:56:52.000', 2);
    INSERT INTO dbo.TST_Location VALUES(37106, 4108, '20130207 17:56:57.000', '20130207 17:57:10.000', 5);
    INSERT INTO dbo.TST_Location VALUES(37199, 4108, '20130207 17:57:11.000', '20130207 17:57:21.000', 2);
    INSERT INTO dbo.TST_Location VALUES(37271, 4108, '20130207 17:57:21.000', '20130207 17:57:31.000', 5);
    INSERT INTO dbo.TST_Location VALUES(37339, 4108, '20130207 17:57:32.000', '20130207 17:57:42.000', 2);
    INSERT INTO dbo.TST_Location VALUES(37412, 4108, '20130207 17:57:44.000', '20130207 17:57:54.000', 6);
    
    Select *
    	,ROW_NUMBER() OVER(PARTITION BY DeviceID
    		ORDER BY logID) AS Grp1
    	,ROW_NUMBER() OVER(PARTITION BY DeviceID, LocationID
    		ORDER BY logID) AS Grp2
    	,ROW_NUMBER() OVER(PARTITION BY DeviceID
    		ORDER BY logID)
    	-
    	ROW_NUMBER() OVER(PARTITION BY DeviceID, LocationID
    		ORDER BY logID) AS Grp
    
    FROM TST_Location
    ORDER BY logID
    I guess this makes it a lot easier for you guys to dive into the problem

    What your code does is aggregate the min or max times over the complete table per location.
    What I need is to aggregate the data each time the location changes.
    So with my example data ROWs:
    1 and 2 should be combined to One as the consecutive LocationID is 5.

    Same would apply to ROWs:
    4 and 5 (both LocationID 4),
    10 and 11 (both LocationID 4),
    15 to 17 (all LocationID 5),
    18 and 19 (both LocationID 2),
    22 and 23 (both LocationID 4),
    24 and 25 (both LocationID 2)

    So the first 5 lines would be compressed to:

    logID deviceID Arrived Departed LocationID Grp1 Grp2 Grp
    34854 4108 2013-02-07 17:51:05.000 2013-02-07 17:51:26.000 5
    35002 4108 2013-02-07 17:51:27.000 2013-02-07 17:51:37.000 3
    35089 4108 2013-02-07 17:51:41.000 2013-02-07 17:52:05.000 4

    Which logID (or any) does not matter in the result.

    I hope this clarifies things a bit.

    Kind rgrds,
    Tom

  4. #4
    Join Date
    Feb 2004
    Location
    In front of the computer
    Posts
    15,579
    Provided Answers: 54
    I'm 99% sure that Itzik Ben-Gan's Grouping Time Intervals is what you need!

    -PatP
    In theory, theory and practice are identical. In practice, theory and practice are unrelated.

  5. #5
    Join Date
    Feb 2013
    Posts
    4
    Pat, thanks for your input. Read the article and it might be solution, however from the text I cannot judge. It is an old article and the related code is no longer present. Do you happen to know if somewhere on the web this code is still present?

    Rgrds, Tom

  6. #6
    Join Date
    Jun 2003
    Location
    Ohio
    Posts
    12,592
    Provided Answers: 1
    This might be one of the cases where a set-based solution is not optimal.

    Here's a method which at least minimized looping. I have implemented it in the past.
    Code:
    /*
    CREATE TABLE dbo.TST_Location
    (
    	[logID] [bigint] NOT NULL,
    	[deviceID] [int] NOT NULL,
    	[Arrived] [datetime] NOT NULL,
    	[Departed] [datetime] NOT NULL,
    	[LocationID] [int] NOT NULL
    ) ON [PRIMARY];
    GO
    
    INSERT INTO dbo.TST_Location VALUES(34854, 4108, '20130207 17:51:05.000', '20130207 17:51:15.000', 5);
    INSERT INTO dbo.TST_Location VALUES(34920, 4108, '20130207 17:51:15.000', '20130207 17:51:26.000', 5);
    INSERT INTO dbo.TST_Location VALUES(35002, 4108, '20130207 17:51:27.000', '20130207 17:51:37.000', 3);
    INSERT INTO dbo.TST_Location VALUES(35089, 4108, '20130207 17:51:41.000', '20130207 17:51:54.000', 4);
    INSERT INTO dbo.TST_Location VALUES(35173, 4108, '20130207 17:51:54.000', '20130207 17:52:05.000', 4);
    INSERT INTO dbo.TST_Location VALUES(35265, 4108, '20130207 17:52:08.000', '20130207 17:52:19.000', 2);
    INSERT INTO dbo.TST_Location VALUES(35335, 4108, '20130207 17:52:19.000', '20130207 17:52:36.000', 3);
    INSERT INTO dbo.TST_Location VALUES(35452, 4108, '20130207 17:52:37.000', '20130207 17:52:49.000', 2);
    INSERT INTO dbo.TST_Location VALUES(35521, 4108, '20130207 17:52:49.000', '20130207 17:53:06.000', 3);
    INSERT INTO dbo.TST_Location VALUES(35635, 4108, '20130207 17:53:07.000', '20130207 17:53:20.000', 4);
    INSERT INTO dbo.TST_Location VALUES(35717, 4108, '20130207 17:53:20.000', '20130207 17:53:30.000', 4);
    INSERT INTO dbo.TST_Location VALUES(35800, 4108, '20130207 17:53:32.000', '20130207 17:53:42.000', 5);
    INSERT INTO dbo.TST_Location VALUES(35867, 4108, '20130207 17:53:43.000', '20130207 17:53:53.000', 6);
    INSERT INTO dbo.TST_Location VALUES(35960, 4108, '20130207 17:53:57.000', '20130207 17:54:10.000', 3);
    INSERT INTO dbo.TST_Location VALUES(36058, 4108, '20130207 17:54:12.000', '20130207 17:54:25.000', 5);
    INSERT INTO dbo.TST_Location VALUES(36159, 4108, '20130207 17:54:28.000', '20130207 17:54:38.000', 5);
    INSERT INTO dbo.TST_Location VALUES(36243, 4108, '20130207 17:54:41.000', '20130207 17:54:51.000', 5);
    INSERT INTO dbo.TST_Location VALUES(36329, 4108, '20130207 17:54:56.000', '20130207 17:55:09.000', 2);
    INSERT INTO dbo.TST_Location VALUES(36433, 4108, '20130207 17:55:11.000', '20130207 17:55:24.000', 2);
    INSERT INTO dbo.TST_Location VALUES(36525, 4108, '20130207 17:55:26.000', '20130207 17:55:39.000', 4);
    INSERT INTO dbo.TST_Location VALUES(36622, 4108, '20130207 17:55:40.000', '20130207 17:55:53.000', 3);
    INSERT INTO dbo.TST_Location VALUES(36717, 4108, '20130207 17:55:56.000', '20130207 17:56:09.000', 4);
    INSERT INTO dbo.TST_Location VALUES(36808, 4108, '20130207 17:56:10.000', '20130207 17:56:23.000', 4);
    INSERT INTO dbo.TST_Location VALUES(36905, 4108, '20130207 17:56:24.000', '20130207 17:56:37.000', 2);
    INSERT INTO dbo.TST_Location VALUES(37004, 4108, '20130207 17:56:39.000', '20130207 17:56:52.000', 2);
    INSERT INTO dbo.TST_Location VALUES(37106, 4108, '20130207 17:56:57.000', '20130207 17:57:10.000', 5);
    INSERT INTO dbo.TST_Location VALUES(37199, 4108, '20130207 17:57:11.000', '20130207 17:57:21.000', 2);
    INSERT INTO dbo.TST_Location VALUES(37271, 4108, '20130207 17:57:21.000', '20130207 17:57:31.000', 5);
    INSERT INTO dbo.TST_Location VALUES(37339, 4108, '20130207 17:57:32.000', '20130207 17:57:42.000', 2);
    INSERT INTO dbo.TST_Location VALUES(37412, 4108, '20130207 17:57:44.000', '20130207 17:57:54.000', 6);
    */
    Select	logID,
    		deviceID,
    		Arrived,
    		Departed,
    		LocationID,
    		ROW_NUMBER() over(order by logID) as RecordID,
    		CONVERT(int, null) as GroupID
    into	#OrderedData
    FROM	TST_Location
    
    update	GroupStarts
    set		GroupID = GroupStarts.RecordID
    from	#OrderedData as GroupStarts
    		left outer join #OrderedData as PriorRecords on GroupStarts.RecordID = PriorRecords.RecordID + 1
    where	GroupStarts.LocationID <> PriorRecords.LocationID
    		or PriorRecords.LocationID is null
    
    while	@@ROWCOUNT > 0
    update	CurrentRecords
    set		GroupID = PriorRecords.GroupID
    from	#OrderedData as CurrentRecords
    		inner join #OrderedData as PriorRecords on CurrentRecords.RecordID = PriorRecords.RecordID + 1
    where	CurrentRecords.GroupID is null
    		and PriorRecords.GroupID is not null
    
    drop table #OrderedData
    If it's not practically useful, then it's practically useless.

    blindman
    www.chess.com: "sqlblindman"
    www.LobsterShot.blogspot.com

  7. #7
    Join Date
    Feb 2004
    Location
    In front of the computer
    Posts
    15,579
    Provided Answers: 54
    This is a wildly summarized version of Itzik's original article which I can't find right now.
    Code:
    WITH L1 AS                  -- Gather raw data
       (  SELECT a.deviceID, a.LocationID, a.Arrived AS t, +1 AS delta
             FROM TST_Location AS a
          UNION ALL 
          SELECT b.deviceID, b.LocationID, b.Departed AS t, -1 AS delta
             FROM TST_Location AS b
       )
    , L2 AS                     -- Add rownumber and simultaneous count
       ( 
       SELECT deviceID, LocationID, t, delta
    ,     Sum(delta) OVER (PARTITION BY deviceID, LocationID ORDER BY t, delta DESC) AS simultaneous
    ,     Row_Number() OVER (ORDER BY DeviceId, LocationID, t, delta DESC) AS r
          FROM L1
       )
    , L3 AS                     -- Compute group ids
       (
       SELECT deviceID, LocationID, t, delta, simultaneous, r
    ,     Count(CASE WHEN delta = simultaneous THEN 1 END) OVER (ORDER BY r) AS g
          FROM L2
       )
       SELECT deviceID, LocationID, Min(t) AS startPeriod, Max(t) AS endPeriod
          FROM L3
    	  GROUP BY deviceID, LocationID, g
    -PatP
    In theory, theory and practice are identical. In practice, theory and practice are unrelated.

  8. #8
    Join Date
    Jun 2003
    Location
    Ohio
    Posts
    12,592
    Provided Answers: 1
    Props for a more elegant, set-based, solution.
    But I wouldn't bet that it would be more efficient than the three or four loops my #TEMP table method would need. I think those ORDER BY statements would sink a query plan against large tables.
    If I have some time, I'll test them out.
    If it's not practically useful, then it's practically useless.

    blindman
    www.chess.com: "sqlblindman"
    www.LobsterShot.blogspot.com

  9. #9
    Join Date
    Nov 2004
    Posts
    1,427
    Provided Answers: 4
    Hey Pat,

    I know what
    Code:
    Row_Number() OVER (ORDER BY DeviceId, LocationID, t, delta DESC) AS r
    does.

    But what does
    Code:
    Sum(delta) OVER (PARTITION BY deviceID, LocationID ORDER BY t, delta DESC) AS simultaneous
    do?
    With kind regards . . . . . SQL Server 2000/2005/2012
    Wim

    Grabel's Law: 2 is not equal to 3 -- not even for very large values of 2.
    Pat Phelan's Law: 2 very definitely CAN equal 3 -- in at least two programming languages

  10. #10
    Join Date
    Feb 2004
    Location
    In front of the computer
    Posts
    15,579
    Provided Answers: 54
    I intentionally architected the CTEs so that they nested in a way that made the process build incrementally... If you cut off the process at any point (say before starting L3) and output the prior level (such as SELECT * FROM L2) you can see what is happening.

    Partitions serve as "group restarts" for the windowed functions. When any of the partition attributes change, the windowed function "starts over" kind of like a control break in a reporting system. If no PARTITION BY is specified, then the whole result set is processed as a single partition (there is no reset).

    The ORDER BY specifies the order to process the rows within the partition.

    -PatP
    In theory, theory and practice are identical. In practice, theory and practice are unrelated.

  11. #11
    Join Date
    Feb 2013
    Posts
    4
    Blindman, Pat thank you for your input.

    I have tried the solution blindman has offered and was able to execute the query.
    I have added the following code just before the drop table statement

    Code:
    SELECT deviceID, LocationID, Min(Arrived) AS Arrived, Max(Departed) AS Departed
          FROM #OrderedData
    	  GROUP BY deviceID, LocationID, GroupID
    ORDER BY Arrived
    And the result I get is what I need. Thanks! I'm still going through the statements to try to fully understand the mechanism.

    I also tried the code from Pat to also get on that learning curve, however I get an execution error on the lines 11 and 18:

    , Sum(delta) OVER (PARTITION BY deviceID, LocationID ORDER BY t, delta DESC) AS simultaneous
    , Count(CASE WHEN delta = simultaneous THEN 1 END) OVER (ORDER BY r) AS g

    Error message:
    Msg 102, Level 15, State 1, Line 11
    Incorrect syntax near 'order'.
    Msg 102, Level 15, State 1, Line 18
    Incorrect syntax near 'order'.

    I'm using SQL Server 2008 Express; SSMS info screen:
    Microsoft SQL Server Management Studio 10.0.5500.0
    Microsoft Data Access Components (MDAC) 6.1.7601.17514
    Microsoft MSXML 3.0 4.0 5.0 6.0
    Microsoft Internet Explorer 9.10.9200.16438
    Microsoft .NET Framework 2.0.50727.5466
    Operating System 6.1.7601

    If possible I would like to benchmark the two solutions on a large table and let you know the result.

    Rgrds, Tom

  12. #12
    Join Date
    Feb 2004
    Location
    In front of the computer
    Posts
    15,579
    Provided Answers: 54
    Unfortunately SQL 2012 is the first version that supports the ORDER BY sub-clause of the OVER clause. You can't use the expression that I posted until you upgrade to SQL 2012.

    You can confirm this at OVER Clause if you are interested.

    -PatP
    In theory, theory and practice are identical. In practice, theory and practice are unrelated.

  13. #13
    Join Date
    Feb 2004
    Location
    In front of the computer
    Posts
    15,579
    Provided Answers: 54
    Note that I should have written that ORDER BY for use with aggregate functions... ORDER BY has had limited support within the OVER clause since SQL 2005.

    -PatP
    In theory, theory and practice are identical. In practice, theory and practice are unrelated.

Tags for this Thread

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •