Consecutively update values starting from a number in the table SQL - sql

I have a question regarding the usage of UPDATE statement. Is there a way for me to code the following scenario with only SQL instead of writing a stored procedure?
I tried to simplify the case.
myTable has 4 columns and its values are as follows:
COMP1 COMP2 NO ACTIVE
0 0 4 N
4 1 Y
2 2 21 Y
3 1 1 Y
1 3 43 Y
2 1 Y
3 1 12 Y
2 2 0 Y
3 2 Y
1 1 5 N
I want to select the values WHERE ACTIVE = 'Y' and start updating
their values in NO column by ordering them by COMP1, COMP2 ASC.
The numbering has to start from the MAX(NO) value in the table WHERE ACTIVE = 'N'
The rows that have same COMP1 and COMP2 will have same NO values.
(Update after comment) NO column is not always NULL. There are erroneous values in this column which are need to be corrected by the update query.
So that, after the UPDATE statement is executed, myTable will be as follows (if we order it)
COMP1 COMP2 NO ACTIVE
0 0 4 N
1 1 5 N
1 3 6 Y
2 1 7 Y
2 2 8 Y
2 2 8 Y
3 1 9 Y
3 1 9 Y
3 2 10 Y
4 1 11 Y
I'm wondering if I would be able to do it without the need to select the whole list and looping in a CURSOR in a stored procedure.
(Update: I've written the SP. A simplified version is as follows. It may have problems since I changed all the names and deleted many conditions.)
PROCEDURE updateAllNo
IS
CURSOR c1
IS
SELECT *
FROM MyTable SE
WHERE SE.ACTIVE = 'Y'
ORDER BY SE.COMP1, SE.COMP2;
v_last_no NUMBER := 0;
v_last_comp2 DATE := SYSDATE + 100;
v_last_comp1 DATE := SYSDATE + 100;
v_now_comp2 DATE := SYSDATE;
v_now_comp1 DATE := SYSDATE;
BEGIN
SELECT MAX (SE.NO)
INTO v_last_no
FROM MyTable SE
WHERE SE.ACTIVE = 'N';
SELECT MAX (SE.COMP1), MAX (SE.COMP2)
INTO v_last_comp1, v_last_comp2
FROM MyTable SE
WHERE SE.ISLEMBASARILI = 'E'
AND SE.NO = v_last_no;
FOR r1 IN c1
LOOP
BEGIN
v_now_comp2 := r1.COMP2;
v_now_comp1 := r1.COMP1;
IF v_now_comp2 != v_last_comp2 OR v_now_comp1 != v_last_comp1
THEN
v_last_no := v_last_no + 1;
END IF;
UPDATE MyTable SE
SET SE.NO = v_last_no
WHERE SEQ_ID = r1.seq_id;
v_last_comp2 := v_now_comp2;
v_last_comp1 := v_now_comp1;
END;
END LOOP;
COMMIT;
END;
I'm using Oracle 11g.

The new number can be retrieved using a window function:
The following will retrieve the active rows and calculate the new value for no
select comp1,
comp2,
row_number() over (order by comp1, comp2) + (select max(no) from mytable where active = 'N') as rn
from mytable
where active = 'Y'
This can now be used in a MERGE statement to run an update against the table. As the table apparently has no PK, I will use the ROWID to match the rows:
merge into mytable tg
using (
select rowid as rid,
comp1,
comp2,
row_number() over (order by comp1, comp2) + (select max(no) from mytable where active = 'N') as rn
from mytable
where active = 'Y'
) t on (t.rid = tg.rowid)
when matched then update
set no = t.rn;
This will most definitely be faster than a loop with single row updates - especially for larger tables.

I am assuming that the field NO is always null when ACTIVE='Y'. If this is the case, then MAX(NO) will return the same value whether or not the WHERE ACTIVE = 'N' condition is specified. With this assumption, this UPDATE statement should do the trick.
UPDATE myTable
SET NO = (SELECT MAX(NO) FROM myTable) + 1
WHERE ROWID IN
(SELECT ROWID FROM myTable
WHERE ACTIVE = 'Y'
ORDER BY COMP1, COMP2);

Related

Fill nulls with lag, but ignore nulls isn't supported

I have a table which looks like this:
ID
money_earned
days_since_start
1
1000
1
1
2000
2
1
null
3
1
3000
4
1
2000
5
2
1000
1
2
null
2
2
100
3
I want that rows, without a value in money_earned (which means that the money_earned column was empty that day) - will and fill the money_earned with last known value, so it to look like this:
ID
money_earned
days_since_start
1
1000
1
1
2000
2
1
2000
3
1
3000
4
1
2000
5
2
1000
1
2
1000
2
2
100
3
I have tried to look up for something like that, but postgresql doesn't support ignore nulls in lag window function :(
thank you!
The LOG() function would be suitable for this purpose, but in your
case you need to get first not null previous element. What the LOG()
function can't handle.
I suggest creating your own function that would return the first non-null value of the previous element.
CREATE OR REPLACE FUNCTION log_not_null(row_num int, dafult text = '1001') RETURNS text LANGUAGE plpgsql AS $$
DECLARE
result text;
index int;
BEGIN
if row_num > 0 then
index := row_num - 1;
else
return dafult;
end if;
result := (select money_earned from my_table offset index limit 1);
if result is null then
return log_not_null(index);
end if;
return result;
END
$$;
select id, log_not_null((row_number() over ())::int) as money_earned, days_since_start from my_table;
Note:
If the previous value is null, then the function is called recursively until it reaches the top element, and if the topmost element is also null, then the function will return the value from the dafult variable.
Demo in sqldaddy.io
You can do this (warning, untested! 😎):
UPDATE yourtable t1
SET money_earned = t3.money_earned
FROM yourtable
LEFT JOIN (SELECT
ID,
MAX(days_since_start) m
FROM yourtable
WHERE ID=t1.ID AND days_since_start<t1.ID) t2 ON t2.ID=i1.ID
INNER JOIN yourtable t3 ON t3.ID=t1.ID and t3.days_since_start = t2.days_since_start
WHERE t1.money_earned is null

How to use SQL (postgresql) query to conditionally change value within each group?

I am pretty new to postgresql (or sql), and have not learned how to deal with such "within group" operation. My data is like this:
p_id number
97313 4
97315 10
97315 10
97325 0
97325 15
97326 4
97335 0
97338 0
97338 1
97338 2
97344 5
97345 14
97349 0
97349 5
p_id is not unique and can be viewed as a grouping variable. I would like to change the number within each p_id to achieve such operation:
if for a given p_id, one of the value is 0, but any of the other "number" for that pid is >2, then set the 0 value as NULL. Like the "p_id" 97325, there are "0" and "15" associated with it. I will replace the 0 by NULL, and keep the other 15 unchanged.
But for p_id 97338, the three rows associated with it have number "0" "1" "2", therefore I do not replace the 0 by NULL.
The final data should be like:
p_id number
97313 4
97315 10
97315 10
97325 NULL
97325 15
97326 4
97335 0
97338 0
97338 1
97338 2
97344 5
97345 14
97349 NULL
97349 5
Thank you very much for the help!
A CASE in a COUNT OVER in a CASE:
SELECT
p_id,
(CASE
WHEN number = 0 AND COUNT(CASE WHEN number > 2 THEN number END) OVER (PARTITION BY p_id) > 0
THEN NULL
ELSE number
END) AS number
FROM yourtable
Test it here on rextester.
Works for PostgreSQL 10:
SELECT p_id, CASE WHEN number = 0 AND maxnum > 2 AND counts >= 2 THEN NULL ELSE number END AS number
FROM
(
SELECT a.p_id AS p_id, a.number AS number, b.maxnum AS maxnum, b.counts AS counts
FROM trans a
LEFT JOIN
(
SELECT p_id, MAX(number) AS maxnum, COUNT(1) AS counts
FROM trans
GROUP BY p_id
) b
ON a.p_id = b.p_id
) a1
use case when
select p_id,
case when p_id>2 and number=0 then null else number end as number
from yourtable
http://sqlfiddle.com/#!17/898c3/1
I would express this as:
SELECT p_id,
(CASE WHEN number <> 0 OR MAX(number) OVER (PARTITION BY p_id) <= 2
THEN number
END) as number
FROM t;
If the fate of a record depends on the existence of other records within (the same or another) table, you could use EXISTS(...) :
UPDATE ztable zt
SET number = NULL
WHERE zt.number = 0
AND EXISTS ( SELECT *
FROM ztable x
WHERE x.p_id = zt.p_id
AND x.number > 2
);

Select rows until condition met

I would like to write an Oracle query which returns a specific set of information. Using the table below, if given an id, it will return the id and value of B. Also, if B=T, it will return the next row as well. If that next row has a B=T, it will return that, and so on until a F is encountered.
So, given 3 it would just return one row: (3,F). Given 4 it would return 3 rows: ((4,T),(5,T),(6,F))
id B
1 F
2 F
3 F
4 T
5 T
6 F
7 T
8 F
Thank you in advance!
Use a sub-query to find out at what point you should stop, then return all row from your starting point to the calculated stop point.
SELECT
*
FROM
yourTable
WHERE
id >= 4
AND id <= (SELECT MIN(id) FROM yourTable WHERE b = 'F' AND id >= 4)
Note, this assumes that the last record is always an 'F'. You can deal with the last record being a 'T' using a COALESCE.
SELECT
*
FROM
yourTable
WHERE
id >= 4
AND id <= COALESCE(
(SELECT MIN(id) FROM yourTable WHERE b = 'F' AND id >= 4),
(SELECT MAX(id) FROM yourTable )
)

SQL - Combining incomplete

I'm using Oracle 10g. I have a table with a number of fields of varying types. The fields contain observations that have been made by made about a particular thing on a particular date by a particular site.
So:
ItemID, Date, Observation1, Observation2, Observation3...
There are about 40 Observations in each record. The table structure cannot be changed at this point in time.
Unfortunately not all the Observations have been populated (either accidentally or because the site is incapable of making that recording). I need to combine all the records about a particular item into a single record in a query, making it as complete as possible.
A simple way to do this would be something like
SELECT
ItemID,
MAX(Date),
MAX(Observation1),
MAX(Observation2)
etc.
FROM
Table
GROUP BY
ItemID
But ideally I would like it to pick the most recent observation available, not the max/min value. I could do this by writing sub queries in the form
SELECT
ItemID,
ObservationX,
ROW_NUMBER() OVER (PARTITION BY ItemID ORDER BY Date DESC) ROWNUMBER
FROM
Table
WHERE
ObservationX IS NOT NULL
And joining all the ROWNUMBER 1s together for an ItemID but because of the number of fields this would require 40 subqueries.
My question is whether there's a more concise way of doing this that I'm missing.
Create the table and the sample date
SQL> create table observation(
2 item_id number,
3 dt date,
4 val1 number,
5 val2 number );
Table created.
SQL> insert into observation values( 1, date '2011-12-01', 1, null );
1 row created.
SQL> insert into observation values( 1, date '2011-12-02', null, 2 );
1 row created.
SQL> insert into observation values( 1, date '2011-12-03', 3, null );
1 row created.
SQL> insert into observation values( 2, date '2011-12-01', 4, null );
1 row created.
SQL> insert into observation values( 2, date '2011-12-02', 5, 6 );
1 row created.
And then use the KEEP clause on the MAX aggregate function with an ORDER BY that puts the rows with NULL observations at the end. whatever date you use in the ORDER BY needs to be earlier than the earliest real observation in the table.
SQL> ed
Wrote file afiedt.buf
1 select item_id,
2 max(val1) keep( dense_rank last
3 order by (case when val1 is not null
4 then dt
5 else date '1900-01-01'
6 end) ) val1,
7 max(val2) keep( dense_rank last
8 order by (case when val2 is not null
9 then dt
10 else date '1900-01-01'
11 end) ) val2
12 from observation
13* group by item_id
SQL> /
ITEM_ID VAL1 VAL2
---------- ---------- ----------
1 3 2
2 5 6
I suspect that there is a more elegant solution to ignore the NULL values than adding the CASE statement to the ORDER BY but the CASE gets the job done.
i dont know about commands in oracle but in sql you could use some how that
first use pivot table is contains consecutives numbers 0,1,2...
i'm not sure but in oracle the function "isnull" is "NVL"
select items.ItemId,
case p.i = 0 then observation1 else '' end as observation1,
case p.i = 0 then observation1 else '' end as observation2,
case p.i = 0 then observation1 else '' end as observation3,
...
case p.i = 39 then observation4 else '' as observation40
from (
select items.ItemId
from table as items
where items.item = _paramerter_for_retrive_only_one_item /* select one item o more item where you filter items here*/
group by items.ItemId) itemgroup
left join
(
select
items.ItemId,
p.i,
isnull( max ( case p.i = 0 then observation1 else '' end ), '' ) as observation1,
isnull( max ( case p.i = 1 then observation2 else '' end ), '' ) as observation2,
isnull( max ( case p.i = 2 then observation3 else '' end), '' ) as observation3,
...
isnull( max ( case p.i = 39 then observation4), '' ) as observation40,
from
(select i from pivot where id < 40 /*you number of columns of observations, that attach one index*/
)
as p
cross join table as items
lef join table as itemcombinations
on item.itemid = itemcombinations.itemid
where items.item = _paramerter_for_retrive_only_one_item /* select one item o more item where you filter items here*/
and (p.i = 0 and not itemcombinations.observation1 is null) /* column 1 */
and (p.i = 1 and not itemcombinations.observation2 is null) /* column 2 */
and (p.i = 2 and not itemcombinations.observation3 is null) /* column 3 */
....
and (p.i = 39 and not itemcombinations.observation3 is null) /* column 39 */
group by p.i, items.ItemId
) as itemsimplified
on itemsimplified.ItemId = itemgroup.itemId
group by itemgroup.itemId
About pivot table
create an pivot table, Take a look at that
pivot table schema
name: pivot columns: {i : datatype int}
How populate
create foo table
schema foo
name: foo column: value datatype varchar
insert into foo
values('0'),
values('1'),
values('2'),
values('3'),
values('4'),
values('5'),
values('6'),
values('7'),
values('8'),
values('9');
/* insert 100 values */
insert into pivot
select concat(a.value, a.value) /* mysql */
a.value + a.value /* sql server */
a.value | a.value /* Oracle im not sure about that sintax */
from foo a, foo b
/* insert 1000 values */
insert into pivot
select concat(a.value, b.value, c.value) /* mysql */
a.value + b.value + c.value /* sql server */
a.value | b.value | c.value /* Oracle im not sure about that sintax */
from foo a, foo b, foo c
the idea about pivot table can consult in "Transact-SQL Cookbook By Jonathan Gennick, Ales Spetic"
I have to admit that the above solution (by Justin Cave) is simpler and easier to understand but this is another good option
at the end like you said you solved

Check that all rows match a given criterion

requestId Consultantid statusid
1 2 10
2 2 10
3 2 10
I want to check if every row has a statusid of 10.
if (every row has a statusid of 10) then
-----do this
endif;
I'm a bit rusty on PL-SQL, but something like this would work in T-SQL:
if not exists (select * from your_table where statusid <> 10) then
-- whatever
end
Edit:
Ok, apparantly in PL-SQL, you need to do something like this:
DECLARE
notallten INTEGER;
BEGIN
SELECT COUNT(*) INTO notallten
FROM your_table
WHERE statusid <> 10
AND ROWNUM = 1;
IF notallten = 0 THEN
-- Do something
END IF;
END;
I don't have an Oracle server to test on though.
declare
v_exists_status_10 number(1);
...
begin
...
-- v_exists_status_10 = 0 if no such row exist, 1 if at least one does
select count(*)
into v_exists_status_10
from dual
where exists
(select * from your_table where statusid <> 10);
if v_exists_status_10 > 0 then
...
Note that you could also do a dumb COUNT() into a variable, but it could be massively inefficient compared to EXISTS. With a COUNT() you'd have to scan all the records, whereas with an EXISTS as soon as it hits a statusid = 10 it can stop scanning.
Simpler solution, that accounts for NULLs in statusid:
for r in (
select 1 dummy
from your_table
where (statusid != 10 or statusid is null)
and rownum = 1) loop
-----do this
end loop;