Move Saturday into Friday then AVG - tsql

I've got a table I'm working with that is about 165M rows of data - Working on creating an average daily usage, but only for the working week. I need to take the inventory transactions from Saturday and count them in Friday and take the Sunday moved into Monday.
CREATE TABLE #temptable ( [ITEMID] nvarchar(20), [Daily Usage] decimal(38,10), [CalendarDate] date )
INSERT INTO #temptable
VALUES
( N'A24519-01', 0.0000000000, N'2019-02-18T00:00:00' ),
( N'A24519-01', 7.0000000000, N'2019-02-19T00:00:00' ),
( N'A24519-01', 10.0000000000, N'2019-02-20T00:00:00' ),
( N'A24519-01', 4.0000000000, N'2019-02-21T00:00:00' ),
( N'A24519-01', 11.0000000000, N'2019-02-22T00:00:00' ),
( N'A24519-01', 0.0000000000, N'2019-02-23T00:00:00' ),
( N'A24519-01', 0.0000000000, N'2019-02-24T00:00:00' ),
( N'A24519-01', 9.0000000000, N'2019-02-25T00:00:00' ),
( N'A24519-01', 5.0000000000, N'2019-02-26T00:00:00' ),
( N'A24519-01', 8.0000000000, N'2019-02-27T00:00:00' ),
( N'A24519-01', 17.0000000000, N'2019-02-28T00:00:00' ),
( N'A24519-01', 0.0000000000, N'2019-03-01T00:00:00' ),
( N'A24519-01', 1.0000000000, N'2019-03-02T00:00:00' ),
( N'A24519-01', 0.0000000000, N'2019-03-03T00:00:00' ),
( N'A24519-01', 1.0000000000, N'2019-03-04T00:00:00' ),
( N'A24519-01', 12.0000000000, N'2019-03-05T00:00:00' ),
( N'A24519-01', 4.0000000000, N'2019-03-06T00:00:00' ),
( N'A24519-01', 14.0000000000, N'2019-03-07T00:00:00' ),
( N'A24519-01', 0.0000000000, N'2019-03-08T00:00:00' ),
( N'A24519-01', 0.0000000000, N'2019-03-09T00:00:00' ),
( N'A24519-01', 0.0000000000, N'2019-03-10T00:00:00' ),
( N'A24519-01', 4.0000000000, N'2019-03-11T00:00:00' ),
( N'A24519-01', 9.0000000000, N'2019-03-12T00:00:00' ),
( N'A24519-01', 6.0000000000, N'2019-03-13T00:00:00' ),
( N'A24519-01', 0.0000000000, N'2019-03-14T00:00:00' ),
( N'A24519-01', 14.0000000000, N'2019-03-15T00:00:00' ),
( N'A24519-01', 1.0000000000, N'2019-03-16T00:00:00' ),
( N'A24519-01', 0.0000000000, N'2019-03-17T00:00:00' )
So if I run the below on the above I get 4.89
SELECT AVG(1 * [Daily Usage])
FROM #temptable
I'm trying to get 6.85 - I don't how to move the number from Sat/Sun into the Fri/Mon - them remove the weekends from the #temptable

If anyone has the same issue and comes here - below it what I ended up using in production of this problem.
--Move Saturday data into Friday and move Sunday data into Monday (Working days for buyers...)
UPDATE
EDU
SET
EDU.[Daily Usage] = EDU.[Daily Usage] + ND.[Daily Usage]
FROM #ExplodedDailyUsage AS EDU
INNER JOIN (SELECT
t.ITEMID
, t.[Daily Usage]
, t.CalendarDate
, DATEPART(dw, t.CalendarDate) DOW
, DATEADD(
DAY, CASE DATEPART(WEEKDAY, t.CalendarDate) WHEN 7 THEN -1 WHEN 1 THEN 1 ELSE 0 END, t.CalendarDate) AS NewDate
FROM #ExplodedDailyUsage AS t
WHERE DATEPART(dw, t.CalendarDate) IN ( 7, 1 )) ND ON ND.NewDate = EDU.CalendarDate
AND ND.ITEMID = EDU.ITEMID;
--Delete Saturdays and Sundays
DELETE FROM #ExplodedDailyUsage WHERE DATEPART(dw, CalendarDate) IN ( 7, 1 );

Related

How to `sum( DISTINCT <column> ) OVER ()` using window function?

I have next data:
Here I already calculated total for conf_id. But want also calculate total for whole partition. eg:
Calculate total suma by agreement for each its order (not goods at order which are with slightly different rounding)
How to sum 737.38 and 1238.3? eg. take only one number among group
(I can not sum( item_suma ), because it will return 1975.67. Notice round for conf_suma as intermediate step)
UPD
Full query. Here I want to calculate rounded suma for each group. Then I need to calculate total suma for those groups
SELECT app_period( '2021-02-01', '2021-03-01' );
WITH
target_date AS ( SELECT '2021-02-01'::timestamptz ),
target_order as (
SELECT
tstzrange( '2021-01-01', '2021-02-01') as bill_range,
o.*
FROM ( SELECT * FROM "order_bt" WHERE sys_period #> sys_time() ) o
WHERE FALSE
OR o.agreement_id = 3385 and o.period_id = 10
),
USAGE AS ( SELECT
ocd.*,
o.agreement_id as agreement_id,
o.id AS order_id,
(dense_rank() over (PARTITION BY o.agreement_id ORDER BY o.id )) as zzzz_id,
(dense_rank() over (PARTITION BY o.agreement_id, o.id ORDER BY (ocd.ic).consumed_period )) as conf_id,
sum( ocd.item_suma ) OVER( PARTITION BY (ocd.o).agreement_id ) AS agreement_suma2,
(sum( ocd.item_suma ) OVER( PARTITION BY (ocd.o).agreement_id, (ocd.o).id, (ocd.ic).consumed_period )) AS x_suma,
(sum( ocd.item_cost ) OVER( PARTITION BY (ocd.o).agreement_id, (ocd.o).id, (ocd.ic).consumed_period )) AS x_cost,
(sum( ocd.item_suma ) OVER( PARTITION BY (ocd.o).agreement_id, (ocd.o).id, (ocd.ic).consumed_period ))::numeric( 10, 2) AS conf_suma,
(sum( ocd.item_cost ) OVER( PARTITION BY (ocd.o).agreement_id, (ocd.o).id, (ocd.ic).consumed_period ))::numeric( 10, 2) AS conf_cost,
max((ocd.ic).consumed) OVER( PARTITION BY (ocd.o).agreement_id, (ocd.o).id, (ocd.ic).consumed_period ) AS consumed,
(sum( ocd.item_suma ) OVER( PARTITION BY (ocd.o).agreement_id, (ocd.o).id )) AS order_suma2
FROM target_order o
LEFT JOIN order_cost_details( o.bill_range ) ocd
ON (ocd.o).id = o.id AND (ocd.ic).consumed_period && o.app_period
)
SELECT
*,
(conf_suma/6) ::numeric( 10, 2 ) as group_nds,
(SELECT sum(x) from (SELECT sum( DISTINCT conf_suma ) AS x FROM usage sub_u WHERE sub_u.agreement_id = usage.agreement_id GROUP BY agreement_id, order_id) t) as total_suma,
(SELECT sum(x) from (SELECT (sum( DISTINCT conf_suma ) /6)::numeric( 10, 2 ) AS x FROM usage sub_u WHERE sub_u.agreement_id = usage.agreement_id GROUP BY agreement_id, order_id) t) as total_nds
FROM USAGE
WINDOW w AS ( PARTITION BY usage.agreement_id ROWS CURRENT ROW EXCLUDE TIES)
ORDER BY
order_id,
conf_id
My old question
I found solution. See dbfiddle.
To run window function for distinct values I should get first value from each peer. To complete this I
aggregate IDs of rows for this peer
lag this aggregation by one
Mark rows that are not aggregated yet (this is first row at peer) as _distinct
sum( ) FILTER ( WHERE _distinct ) over ( ... )
Voila. You get sum over DISTINCT values at target PARTITION
which are not implemented yet by PostgreSQL
with data as (
select * from (values
( 1, 1, 1, 1.0049 ), (2, 1,1,1.0049), ( 3, 1,1,1.0049 ) ,
( 4, 1, 2, 1.0049 ), (5, 1,2,1.0057),
( 6, 2, 1, 1.53 ), ( 7,2,1,2.18), ( 8,2,2,3.48 )
) t (id, agreement_id, order_id, suma)
),
intermediate as (select
*,
sum( suma ) over ( partition by agreement_id, order_id ) as fract_order_suma,
sum( suma ) over ( partition by agreement_id ) as fract_agreement_total,
(sum( suma::numeric(10,2) ) over ( partition by agreement_id, order_id )) as wrong_order_suma,
(sum( suma ) over ( partition by agreement_id, order_id ))::numeric( 10, 2) as order_suma,
(sum( suma ) over ( partition by agreement_id ))::numeric( 10, 2) as wrong_agreement_total,
id as xid,
array_agg( id ) over ( partition by agreement_id, order_id ) as agg
from data),
distinc as (select *,
lag( agg ) over ( partition by agreement_id ) as prev,
id = any (lag( agg ) over ()) is not true as _distinct, -- allow to match first ID from next peer
order_suma as xorder_suma, -- repeat column to easily visually compare with _distinct
(SELECT sum(x) from (SELECT sum( DISTINCT order_suma ) AS x FROM intermediate sub_q WHERE sub_q.agreement_id = intermediate.agreement_id GROUP BY agreement_id, order_id) t) as correct_total_suma
from intermediate
)
select
*,
sum( order_suma ) filter ( where _distinct ) over ( partition by agreement_id ) as also_correct_total_suma
from distinc
better approach dbfiddle:
Assign row_number at each order: row_number() over (partition by agreement_id, order_id ) as nrow
Take only first suma: filter nrow = 1
with data as (
select * from (values
( 1, 1, 1, 1.0049 ), (2, 1,1,1.0049), ( 3, 1,1,1.0049 ) ,
( 4, 1, 2, 1.0049 ), (5, 1,2,1.0057),
( 6, 2, 1, 1.53 ), ( 7,2,1,2.18), ( 8,2,2,3.48 )
) t (id, agreement_id, order_id, suma)
),
intermediate as (select
*,
row_number() over (partition by agreement_id, order_id ) as nrow,
(sum( suma ) over ( partition by agreement_id, order_id ))::numeric( 10, 2) as order_suma,
from data)
select
*,
sum( order_suma ) filter (where nrow = 1) over (partition by agreement_id)
from intermediate```

First and Last time with Night Labor time

I have this issue.
These are the data that I have in the sample (I have more people and more times):
CREATE TABLE table_times(id,date_time,name)
AS ( VALUES
( 1000004, '2018-08-22 11:11'::timestamp without time zone, 'Carlos Eduardo' ),
( 1000004, '2018-08-22 11:43', 'Carlos Eduardo' ),
( 1000004, '2018-08-22 11:48', 'Carlos Eduardo' ),
( 1000004, '2018-08-22 11:54', 'Carlos Eduardo' ),
( 1000004, '2018-08-22 17:52', 'Carlos Eduardo' ),
( 1000004, '2018-08-23 08:13', 'Carlos Eduardo' ),
( 1000004, '2018-08-23 08:28', 'Carlos Eduardo' ),
( 1000004, '2018-08-23 10:25', 'Carlos Eduardo' ),
( 1000004, '2018-08-23 10:25', 'Carlos Eduardo' ),
( 1000004, '2018-08-23 10:25', 'Carlos Eduardo' ),
( 1000004, '2018-08-23 13:30', 'Carlos Eduardo' ),
( 1000004, '2018-08-24 22:20', 'Carlos Eduardo' ),
( 1000004, '2018-08-24 23:27', 'Carlos Eduardo' ),
( 1000004, '2018-08-25 03:14', 'Carlos Eduardo' ),
( 1000004, '2018-08-25 05:12', 'Carlos Eduardo' )
);
And I'm trying to find this:
id start end name
-------+-------------------+-------------------+--------------
1000004 2018-08-22 11:11:00 2018-08-22 17:52:00 Carlos Eduardo
1000004 2018-08-23 08:13:00 2018-08-23 13:30:00 Carlos Eduardo
1000004 2018-08-24 22:20:00 2018-08-25 05:12:00 Carlos Eduardo
I need to organize these data per date, like the days 22, 23 and organize the time when start at night and finish at morning, like 24 and 25.
I have some other people and they don't have a specific labor time then I need to get the time by these data. Besides that, I have just these data to organize and the maximum of 14 labor hours per day.
I tried the query below, but when the labor time is at night, it doesn't work.
SELECT
id,
MIN(date_time) AS start,
MAX(date_time) AS end,
name
FROM
table_times
GROUP BY
id
, name
, EXTRACT(DAY FROM date_time)
, EXTRACT(MONTH FROM date_time)
, EXTRACT(YEAR FROM date_time)
ORDER BY name, start ASC
May anyone help me?
PS: Sorry my poor english.
In order to make your problem tractable, we will need to define the boundaries for a shift. In this answer, I assume that you have two kinds of shifts. Day shifts, which start after 06:00 and end before 22:00, always occur within the same calendar day. Night shifts, which start after 22:00 and end before 06:00, wrap around across two days.
I employ an accounting trick below, by which I treat all night shift timestamps as logically belonging to the same starting date. This allows us to handle your boundary conditions.
WITH cte AS (
SELECT
id,
date_time,
name,
CASE WHEN EXTRACT(HOUR FROM date_time) >= 22 OR EXTRACT(HOUR FROM date_time) <= 6
THEN 1 ELSE 0 END AS shift,
CASE WHEN EXTRACT(HOUR FROM date_time) <= 6
THEN date_time::date - INTERVAL '1 DAY'
ELSE date_time::date END AS logical_date
FROM table_times
)
SELECT
id,
MIN(date_time) AS start,
MAX(date_time) AS end,
name
FROM cte
GROUP BY
id,
shift,
name,
logical_date
ORDER BY
name,
start;
Demo

Getting unique months using group by

I have a table called tbl_points with the following columns:
[key] identity
[fkey] int -> forign key from other table
[points] int -> number
[inputdate] datetime -> getdate()
And values like:
key,fkey,points,inputdate
1,23,5,20170731
2,23,2,20170801
3,23,4,20170801
4,25,2,20170801
5,25,2,20170802
6,23,5,20170803
7,25,3,20170803
8,23,5,20170804
I am executing a query like this:
select fkey,sum(points) points,month(inputdate) mnd,year(inputdate) yy
from tbl_points
group by fkey,month(inputdate) mnd,year(inputdate)
order by year(inputdate),month(inputdate) mnd,points
Which gives as result:
fkey,points,mnd,yy
23,14,8,2017
25,7,8,2017
25,5,7,2017
So far so good. Now I want only the top 1 of each month, so
23,14,8,2017
25,5,7,2017
I can do this in the code, or in the stored procedure with a temporary table or cursor.
But perhaps there is is simpler solution. Any ideas? Or a better approach?
DECLARE #tbl_points TABLE
(
[key] INT ,
[fkey] INT ,
[points] INT ,
[inputdate] DATETIME
);
INSERT INTO #tbl_points
VALUES ( 1, 23, 5, '2017-07-31' ),
( 2, 23, 2, '2017-08-01' ),
( 3, 23, 4, '2017-08-01' ),
( 4, 25, 2, '2017-08-01' ),
( 5, 25, 2, '2017-08-02' ),
( 6, 23, 5, '2017-08-03' ),
( 7, 25, 3, '2017-08-03' ),
( 8, 23, 5, '2017-08-04' );
/* Your query */
SELECT fkey ,
SUM(points) points ,
YEAR(inputdate) [year] ,
MONTH(inputdate) [month]
FROM #tbl_points
GROUP BY fkey ,
MONTH(inputdate) ,
YEAR(inputdate)
ORDER BY YEAR(inputdate) ,
MONTH(inputdate) ,
points;
/* Query you want */
SELECT s.fkey ,
s.points ,
s.[year] ,
s.[month]
FROM ( SELECT fkey ,
SUM(points) points ,
YEAR(inputdate) [year] ,
MONTH(inputdate) [month] ,
ROW_NUMBER() OVER ( PARTITION BY MONTH(inputdate) ORDER BY YEAR(inputdate) , MONTH(inputdate) , SUM(points) ASC ) [Row]
FROM #tbl_points
GROUP BY fkey ,
MONTH(inputdate) ,
YEAR(inputdate)
) AS s
WHERE s.Row = 1;
Result:

Rank result set according to condition

I have a table which has 3 columns: Product, Date, Status
I want to rank in this manner:
for each product order by Date, and Rank if Status = FALSE then 0, if it's TRUE then start ranking by 1, continue ranking by the same value if previous Status is TRUE.
In this ordered set if FALSE comes assign to it 0, and for the next coming TRUE status for same product assign x+1 (x here is previous rank value for status TRUE).
I hope picture makes it more clear
This code uses SS2008R2 features which do not include LEAD/LAG. A better solution is certainly possible with more modern versions of SQL Server.
-- Sample data.
declare #Samples as Table ( Product VarChar(10), ProductDate Date,
ProductStatus Bit, DesiredRank Int );
insert into #Samples values
( 'a', '20160525', 0, 0 ), ( 'a', '20160526', 1, 1 ), ( 'a', '20160529', 1, 1 ),
( 'a', '20160601', 1, 1 ), ( 'a', '20160603', 0, 0 ), ( 'a', '20160604', 0, 0 ),
( 'a', '20160611', 1, 2 ), ( 'a', '20160612', 0, 0 ), ( 'a', '20160613', 1, 3 ),
( 'b', '20160521', 1, 1 ), ( 'b', '20160522', 0, 0 ), ( 'b', '20160525', 1, 2 );
select * from #Samples;
-- Query to rank data as requested.
with WithRN as (
select Product, ProductDate, ProductStatus, DesiredRank,
Row_Number() over ( partition by Product order by ProductDate ) as RN
from #Samples
),
RCTE as (
select *, Cast( ProductStatus as Int ) as C
from WithRN
where RN = 1
union all
select WRN.*, C + Cast( 1 - R.ProductStatus as Int ) * Cast( WRN.ProductStatus as Int )
from RCTE as R inner join
WithRN as WRN on WRN.Product = R.Product and WRN.RN = R.RN + 1 )
select Product, ProductDate, ProductStatus, DesiredRank,
C * ProductStatus as CalculatedRank
from RCTE
order by Product, ProductDate;
Note that the sample data was extracted from an image using a Mark I Eyeball. Had the OP taken heed of advice here it would have been somewhat easier.
Tip: Using column names that don't happen to match data types and keywords makes life somewhat simpler.
Try this query,
SELECT a.Product ,
a.Date ,
a.Status ,
CASE WHEN a.Status = 'FALSE' THEN 0
ELSE 1
END [Rank]
FROM ( SELECT Product ,
Date ,
Status ,
ROW_NUMBER() OVER ( PARTITION BY Product ORDER BY DATE, Status ) RNK
FROM TableProduct
) a
ORDER BY Product, a.RNK

rearrage lines of file in unix

I have a file with the below format. This is a snippet, original lines cross over a million.
Infact ABC/DEF/GH9j/etc.. is the starting of a line and ";" is supposed to be the end of the line.
Here in this example line01, line10 & line13 are perfect; But other lines are scattered in to multiple lines:
line01: ABC abc_123 ( .Y ( B2b ) , .A ( sel ) );
line02: DEF def_456 ( .Z ( n_2 ) , .in ( 1b0 ) ,
line03: .tstin ( sel ) , .tstmb ( DD ) );
line04: GH9j 3_inst ( .Q0 ( CC3 ) , .Q1 ( Ee ) ,
line05: .Q2 ( p_2 ) , .Q3 ( cin ) ,
line06: .D0 ( AA ) , .D1 ( rdata[5] ) ,
line07: .D2 ( gg ) , .D3 ( hp ) ,
line08: .SE0 ( sel ) , .SE1 ( sel ) ,
line09: .SE2 ( pqr ) , .SE3 ( AA ) , .CK ( Bb ) );
line10: BUF 4PQR ( .Y ( eE ) , .A ( cC ) );
line11: MX2 MnOp ( .X ( DD ) , .A ( PQR_11 ) ,
line12: .B ( trstb ) , .S0 ( klm2 ) );
line13: BUFH 6th_inst ( .Zz ( AA ) , .A ( B2B ) );
.......
Q. I want to rearrange all lines like below:
All the ports of one instance should be in ONE LINE (13 lines should reduce to 6 lines)
line01: ABC abc_123 ( .Y ( B2b ) , .A ( sel ) );
line02: DEF def_456 ( .Z ( n_2 ) , .in ( 1b0 ) , .tstin ( sel ) , .tstmb ( DD ) );
line03: GH9j 3_inst ( .Q0 ( CC3 ) , .Q1 ( Ee ) , .Q2 ( p_2 ) , .Q3 ( cin ) , .D0 ( AA ) , .D1 ( rdata[5] ) , .D2 ( gg ) , .D3 ( hp ) , .SE0 ( sel ) , .SE1 ( sel ) , .SE2 ( pqr ) , .SE3 ( AA ) , .CK ( Bb ) );
line04: BUF 4PQR ( .Y ( eE ) , .A ( cC ) );
line05: MX2 MnOp ( .X ( DD ) , .A ( PQR_11 ) , .B ( trstb ) , .S0 ( klm2 ) );
line06: BUFH 6th_inst ( .Zz ( AA ) , .A ( B2B ) );
A one liner might work: perl -pe' chomp unless m/\;/' file.txt - though with millions of lines some tweaking might be needed.
Here is the one liner as a script:
#!/usr/bin/env perl
while (<DATA>) {
chomp unless m/\;/ ;
print ;
}
__DATA__
ABC abc_123 ( .Y ( B2b ) , .A ( sel ) );
DEF def_456 ( .Z ( n_2 ) , .in ( 1b0 ) ,
.tstin ( sel ) , .tstmb ( DD ) );
GH9j 3_inst ( .Q0 ( CC3 ) , .Q1 ( Ee ) ,
.Q2 ( p_2 ) , .Q3 ( cin ) ,
.D0 ( AA ) , .D1 ( rdata[5] ) ,
.D2 ( gg ) , .D3 ( hp ) ,
.SE0 ( sel ) , .SE1 ( sel ) ,
.SE2 ( pqr ) , .SE3 ( AA ) , .CK ( Bb ) );
BUF 4PQR ( .Y ( eE ) , .A ( cC ) );
MX2 MnOp ( .X ( DD ) , .A ( PQR_11 ) ,
.B ( trstb ) , .S0 ( klm2 ) );
BUFH 6th_inst ( .Zz ( AA ) , .A ( B2B ) );
Output:
ABC abc_123 ( .Y ( B2b ) , .A ( sel ) );
DEF def_456 ( .Z ( n_2 ) , .in ( 1b0 ) ,.tstin ( sel ) , .tstmb ( DD ) );
GH9j 3_inst ( .Q0 ( CC3 ) , .Q1 ( Ee ) ,.Q2 ( p_2 ) , .Q3 ( cin ) ,.D0 ( AA ) , .D1 ( rdata[5] ) ,.D2 ( gg ) , .D3 ( hp ) ,.SE0 ( sel ) , .SE1 ( sel ) ,.SE2 ( pqr ) , .SE3 ( AA ) , .CK ( Bb ) );
BUF 4PQR ( .Y ( eE ) , .A ( cC ) );
MX2 MnOp ( .X ( DD ) , .A ( PQR_11 ) ,.B ( trstb ) , .S0 ( klm2 ) );
BUFH 6th_inst ( .Zz ( AA ) , .A ( B2B ) );
If you need to preserve the line numbers (line01:) add a note to that effect in the question.
Here is a sed solution:
sed -n 'H;/;/{s/.*//;x;s/\n//g;p;}' filename
Without seeing your whole dataset, or at least large chuncks of it, its hard to know what pattern to match on, however in the example you give, it seems as though the lines you want to merge end in a ,newline therefore simply changing ,newline to , might do the trick:
sed -e ':begin;$!N;s/,\n/,/;tbegin;P;D' in.txt
outputs:
ABC abc_123 ( .Y ( B2b) , .A ( sel));
DEF def_456 ( .Z ( n_2) , .in ( 1b0) ,.tstin ( sel) , .tstmb ( DD));
GH9j 3_inst ( .Q0 ( CC3) , .Q1 ( Ee) ,.Q2 ( p_2) , .Q3 ( cin) ,.D0 ( AA) , .D1 ( rdata [5]) ,.D2 ( gg) , .D3 ( hp) ,.SE0 ( sel) , .SE1 ( sel) ,.SE2 ( pqr) , .SE3 ( AA) , .CK ( Bb));
BUF 4PQR ( .Y ( eE) , .A ( cC));
MX2 MnOp ( .X ( DD) , .A ( PQR_11) ,.B ( trstb) , .S0 ( klm2));
BUFH 6th_inst ( .Zz ( AA) , .A ( B2B));
See http://backreference.org/2009/12/23/how-to-match-newlines-in-sed/ for more details on matching newlines in sed.
A one-liner in AWK
awk '{printf "%s%s", $0, /;\s*$/ ? "\n" : " "}' filename
It outputs each line with a separator that is either a newline or space depending on whether ; is found at the end of the line.