PostgreSQL UPDATE FROM - postgresql

Apologies if this has been answered elsewhere, I'm afraid I need a little more clarification/brushing up on the UPDATE FROM clause in PostgreSQL.
Basically I have a temporary table with some intermediary computed stuff that I want to use to update the main table. This temporary table includes two foreign keys and a score, such as:
score fk_offer fk_searchprofile
65 1764 12345
...
I tested the rows to be updated with a select (the table temp_offerids_with_score contains the offers that need to be updated):
SELECT s.pkid, tmp.fk_offer, s.fk_category, tmp.score, tmp.fk_searchprofile
FROM
temp_weighted_scores_offers AS tmp
INNER JOIN sc_sp_o_c_score AS s
ON tmp.fk_offer = s.fk_offer
WHERE
tmp.fk_offer IN (SELECT fk_offer FROM temp_offerids_with_score)
AND
s.fk_category = 1
AND s.fk_searchprofile = 12345;
This correctly returns the expected number of rows (in this case 10):
pkid fk_offer fk_category score fk_searchprofile
1 47 1 78 12345
2 137 1 64 12345
3 247 1 50 12345
...
However, if I use the same in an UPDATE FROM:
UPDATE sc_sp_o_c_score
SET score = tmp.score
FROM
temp_weighted_scores_offers AS tmp
INNER JOIN sc_sp_o_c_score AS s
ON tmp.fk_offer = s.fk_offer
WHERE
tmp.fk_offer IN (SELECT fk_offer FROM temp_offerids_with_score)
AND
s.fk_category = 1
AND s.fk_searchprofile = 12345;
the whole table, over 32000 rows, gets updated with the same (wrong, of course) score overall.
pkid fk_offer fk_searchprofile fk_category score
1 47 12345 1 104
2 137 12345 1 104
3 247 12345 1 104
What am I missing?
Thanks, Julian
EDIT: just in case this could be of any help - for the record, I'm migrating things from SQL Server here, where this is in fact a valid construct.

You are using the table to be updated also as a self-join (through reference in the FROM clause). Take that out and you should be good:
UPDATE sc_sp_o_c_score
SET score = tmp.score
FROM temp_weighted_scores_offers AS tmp
WHERE tmp.fk_offer = fk_offer
AND tmp.fk_offer IN (SELECT fk_offer FROM temp_offerids_with_score)
AND fk_category = 1
AND fk_searchprofile = 12345;

Related

Taking N-samples from each group in PostgreSQL

I have a table containing data that has a column named id that looks like below:
id
value 1
value 2
value 3
1
244
550
1000
1
251
551
700
1
540
60
1200
...
...
...
...
2
19
744
2000
2
10
903
100
2
44
231
600
2
120
910
1100
...
...
...
...
I want to take 50 sample rows per id that exists but if less than 50 exist for the group to simply take the entire set of data points.
For example I would like a maximum 50 data points randomly selected from id = 1, id = 2 etc...
I cannot find any previous questions similar to this but have tried taking a stab at at least logically working through the solution where I could iterate and union all queries by id and limit to 50:
SELECT * FROM (SELECT * FROM schema.table AS tbl WHERE tbl.id = X LIMIT 50) UNION ALL;
But it's obvious that you cannot use this type of solution because UNION ALL requires aggregating outputs from one id to the next and I do not have a list of id values to use in place of X in tbl.id = X.
Is there a way to accomplish this by gathering that list of unique id values and union all results or is there a more optimal way this could be done?
If you want to select a random sample for each id, then you need to randomize the rows somehow. Here is a way to do it:
select * from (
select *, row_number() over (partition by id order by random()) as u
from schema.table
) as a
where u <= 50;
Example (limiting to 3, and some row number for each id so you can see the selection randomness):
setup
DROP TABLE IF EXISTS foo;
CREATE TABLE foo
(
id int,
value1 int,
idrow int
);
INSERT INTO foo
select 1 as id, (1000*random())::int as value1, generate_series(1, 100) as idrow
union all
select 2 as id, (1000*random())::int as value1, generate_series(1, 100) as idrow
union all
select 3 as id, (1000*random())::int as value1, generate_series(1, 100) as idrow;
Selection
select * from (
select *, row_number() over (partition by id order by random()) as u
from foo
) as a
where u <= 3;
Output:
id
value1
idrow
u
1
542
6
1
1
24
86
2
1
155
74
3
2
505
95
1
2
100
46
2
2
422
33
3
3
966
88
1
3
747
89
2
3
664
19
3
In case you are looking to get 50 (or less) from each group of IDs then you can use windowing -
From question - "I want to take 50 sample rows per id that exists but if less than 50 exist for the group to simply take the entire set of data points."
Query -
with data as (
select row_number() over (partition by id order by random()) rn,
* from table_name)
select * from data where rn<=50 order by id;
Fiddle.
Your description of trying to get the UNION ALL without specifying all the branches ahead of time is aiming for a LATERAL join. And that is one way to solve the problem. But unless you have a table of all distinct ids, you would have to compute one on the fly. For example (using the same fiddle as Pankaj used):
with uniq as (select distinct id from test)
select foo.* from uniq cross join lateral
(select * from test where test.id=uniq.id order by random() limit 3) foo
This could be either slower or faster than the Window Function method, depending on your system and your data and your indexes. In my hands, it was quite a bit faster even with the need to dynamically compute the list of distinct ids.

T-SQL: Combining rows based on another table

I am seeking to alter a table content based on information of another table using a stored procedure. To make my point (and dodge my rusty English skills) I created the following simplification.
I have a table with fragment amounts of the form
SELECT * FROM [dbo].[obtained_fragments] ->
fragment amount
22 42
76 7
101 31
128 4
177 22
212 6
and a table that lists all possible combinations to combine these fragments to other fragments.
SELECT * FROM [dbo].[possible_combinations] ->
fragment consists_of_f1 f1_amount_needed consists_of_f2 f2_amount_needed
1001 128 1 22 3
1004 151 1 101 12
1012 128 1 177 6
1047 212 1 76 4
My aim is to alter the first table so that all possible fragment combinations are performed, leading to
SELECT * FROM [dbo].[obtained_fragments] ->
fragment amount
22 30
76 3
101 31
177 22
212 5
1001 4
1047 1
In words, combined fragments are added to the table based on [dbo].[possible_combinations], and the amount of needed fragments is reduced. Depleted fragments are removed from the table.
How do I achieve this fragment transformation in an easy way? I started writing a while loop, checking if sufficient fragments are available, inside of a for loop, interating through the fragment numbers. However, I am unable to come up with a functional amount check and begin to wonder if this is even possible in T-SQL this way.
The code doesn't have to be super efficient since both tables will always be smaller than 200 rows.
It is important to note that it doesn't matter which combinations are created.
It might come in handy that [f1_amount_needed] always has a value of 1.
UPDATE
Using the solution of iamdave, which works perfectly fine as long I don't touch it, I receive the following error message:
Column name or number of supplied values does not match table definition.
I barely changed anything really. Is there a chance that using existing tables with more than the necessary columns instead of declaring the tables (as iamdave did) makes this difference?
DECLARE #t TABLE(Binding_ID int, Exists_of_Binding_ID_2 int, Exists_of_Pieces_2 int, Binding1 int, Binding2 int);
WHILE 1=1
BEGIN
DELETE #t
INSERT INTO #t
SELECT TOP 1
k.Binding_ID
,k.Exists_of_Binding_ID_2
,k.Exists_of_Pieces_2
,g1.mat_Binding_ID AS Binding1
,g2.mat_Binding_ID AS Binding2
FROM [dbo].[vwCombiBinding] AS k
JOIN [leer].[sandbox5] AS g1
ON k.Exists_of_Binding_ID_1 = g1.mat_Binding_ID AND g1.Amount >= 1
JOIN [leer].[sandbox5] AS g2
ON k.Exists_of_Binding_ID_2 = g2.mat_Binding_ID AND g2.Amount >= k.Exists_of_Pieces_2
ORDER BY k.Binding_ID
IF (SELECT COUNT(1) FROM #t) = 1
BEGIN
UPDATE g
SET Amount = g.Amount +1
FROM [leer].[sandbox5] AS g
JOIN #t AS t
ON g.mat_Binding_ID = t.Binding_ID
INSERT INTO [leer].[sandbox5]
SELECT
t.Binding_ID
,1
FROM #t AS t
WHERE NOT EXISTS (SELECT NULL FROM [leer].[sandbox5] AS g WHERE g.mat_Binding_ID = t.Binding_ID);
UPDATE g
SET Amount = g.Amount - 1
FROM [leer].[sandbox5] AS g
JOIN #t AS t
ON g.mat_Binding_ID = t.Binding1
UPDATE g
SET Amount = g.Amount - t.Exists_of_Pieces_2
FROM [leer].[sandbox5] AS g
JOIN #t AS t
ON g.mat_Binding_ID = t.Binding2
END
ELSE
BREAK
END
SELECT * FROM [leer].[sandbox5]
You can do this with a while loop that contains several statements to handle your iterative data updates. As you need to make changes based on a re-assessment of your data each iteration this has to be done in a loop of some kind:
declare #f table(fragment int,amount int);
insert into #f values (22 ,42),(76 ,7 ),(101,31),(128,4 ),(177,22),(212,6 );
declare #c table(fragment int,consists_of_f1 int,f1_amount_needed int,consists_of_f2 int,f2_amount_needed int);
insert into #c values (1001,128,1,22,3),(1004,151,1,101,12),(1012,128,1,177,6),(1047,212,1,76,4);
declare #t table(fragment int,consists_of_f2 int,f2_amount_needed int,fragment1 int,fragment2 int);
while 1 = 1
begin
-- Clear out staging area
delete #t;
-- Populate with the latest possible combination
insert into #t
select top 1 c.fragment
,c.consists_of_f2
,c.f2_amount_needed
,f1.fragment as fragment1
,f2.fragment as fragment2
from #c as c
join #f as f1
on c.consists_of_f1 = f1.fragment
and f1.amount >= 1
join #f as f2
on c.consists_of_f2 = f2.fragment
and f2.amount >= c.f2_amount_needed
order by c.fragment;
-- Update fragments table if a new combination can be made
if (select count(1) from #t) = 1
begin
-- Update if additional fragment
update f
set amount = f.amount + 1
from #f as f
join #t as t
on f.fragment = t.fragment;
-- Insert if a new fragment
insert into #f
select t.fragment
,1
from #t as t
where not exists(select null
from #f as f
where f.fragment = t.fragment
);
-- Update fragment1 amounts
update f
set amount = f.amount - 1
from #f as f
join #t as t
on f.fragment = t.fragment1;
-- Update fragment2 amounts
update f
set amount = f.amount - t.f2_amount_needed
from #f as f
join #t as t
on f.fragment = t.fragment2;
end
else -- If no new combinations possible, break the loop
break
end;
select *
from #f;
Output:
+----------+--------+
| fragment | amount |
+----------+--------+
| 22 | 30 |
| 76 | 3 |
| 101 | 31 |
| 128 | 0 |
| 177 | 22 |
| 212 | 5 |
| 1001 | 4 |
| 1047 | 1 |
+----------+--------+

PostgreSQL non-overlapping ranges

I use PostgreSQL database and have a cards table.
Each record(card) in this table have card_drop_rate integer value.
For example:
id | card_name |card_drop_rate
-------------------------------
1 |card1 |34
2 |card2 |16
3 |card3 |54
max drop rate is 34 + 16 + 54 = 104.
In accordance to my application logic I need to find a random value between 0 and 104 and then retrieve card according to this number, for example:
random value: 71
card1 range: 0 - 34(0 + 34)
card2 range: 34 - 50(34 + 16)
card3 range: 50 - 104(50 + 54)
So, my card is card3 because 71 is placed in the range 50 - 104
What is the proper way to reflect this structure in PostgreSQL ? I'll need to query this data often under so the performance is a criterion number one for this solution.
Following query works fine:
SELECT
b.id,
b.card_drop_rate
FROM (SELECT a.id, sum(a.card_drop_rate) OVER(ORDER BY id) - a.card_drop_rate as rate, card_drop_rate FROM cards as a) b
WHERE b.rate < 299 ORDER BY id DESC LIMIT 1
You can do this using cumulative sums and random. The "+ 1"s might be throwing me off, but it is something like this:
with c as (
select c.*,
sum(card_drop_rate + 1) - card_drop_rate as threshhold
from cards c
),
r as (
select random() * (sum(card_drop_rate) + count(*) - 1) as which_card
from cards c
)
select c.*
from c cross join
r
where which_card >= threshhold
order by threshhold
limit 1;
For performance, I would simply take the cards and generate a new table with 106 slots. Assign the card value to the slots and build an index on the slot number. Then get a value using:
select s.*
from slots s
where s.slotid = floor(random() * 107);

select two maximum values per person based on a column partition

Hi if I have the following table:
Person------Score-------Score_type
1 30 A
1 35 A
1 15 B
1 16 B
2 74 A
2 68 A
2 40 B
2 39 B
Where for each person and score type I want to pick out the maximum score to obtain a table like:
Person------Score-------Score_type
1 35 A
1 16 B
2 74 A
2 40 B
I can do this using multiple select statements, but this will be cumbersome, especially later on. so I was wondering if there is a function which can help me do this. I have used the parititon function before but only to label sequences in a table....
select person,
score_type,
max(score) as score
from scores
group by person, score_type
order by person, score_type;
With "partition function" I guess you mean window functions. They can indeed be used for this as well:
select person
score_type,
score
from (
select person,
score_type,
score,
row_number() over (partition by person, score_type order by score desc) as rn
from scores
) t
where rn = 1
order by person, score_type;
Using the max() aggregate function along with the grouping by person and score_type should do the trick.

sybase update based on sequential column

Can any one of you please help me out with the below condition in sybase update?
Table A
ID COL1 COL1_AMT COL2 COL2_AMT COL3 COL3_AMT COL4 COL4_AMT
1 10 100.00 16 50.00 17 80.00 21 90.00
Table B
ID FIN_AMT
1 20
Whenever COL1 or COL2 or COL3 or COL4 equals value as '17' then the corresponding amount column COL1_AMT or COL2_AMT or COL3_AMT or COL4_AMT value should be picked from Table A and get updated in Table B FIN_AMT clumn
Here COL3 is 17, so COL3_AMT value 80 should be added in Table B FIN_AMT
Expected Result in Table B
ID FIN_AMT
1 100 (Already 20 is there, so 80 should be added to this)
Thanks in advance
I can't test on SYBASE, and it's been years, so have mercy on the finer details of the syntax ;)
Something like this ought to do it;
UPDATE TableB
SET TableB.FIN_AMT = TableB.FIN_AMT +
CASE WHEN TableA.COL1 = 17 THEN TableA.COL1_AMT ELSE 0 END +
CASE WHEN TableA.COL2 = 17 THEN TableA.COL2_AMT ELSE 0 END +
CASE WHEN TableA.COL3 = 17 THEN TableA.COL3_AMT ELSE 0 END +
CASE WHEN TableA.COL4 = 17 THEN TableA.COL4_AMT ELSE 0 END
FROM TableB, TableA
WHERE TableB.ID = TableA.ID