PostgreSQL: Updating column in a table based on another indirectly referenced table - postgresql

After adding a new column new_column to an existing table rules, I would like to apply some complex migration logic in order to determine the new value for each row (I need to look at another table called flags which is indirectly referenced by rules).
My questions:
Q1. Can I accomplish this with JOIN and a CASE statement, or would I need to write a pgSQL function?
Q2. Which of the logical approaches is better (pseudo code):
FOR flag IN flags
FOR app IN applications WHERE app.accountid = flag.accountid
FOR campaign IN campaigns WHERE campaign.applicationid = app.id
FOR rule IN rules WHERE rule.campaignid = campaign.id
SET rule.new_column TO
(CASE flag.new == true AND flag.old == false THEN ‘v2’
CASE flag.new == false AND flag.old == true THEN ‘v1’
ELSE return ‘v0’)
Or is it better to go about it this way:
FOR rule IN rules
SELECT campaign FROM campaigns WHERE campaign.id = rule.campaignid
SELECT app FROM applications WHERE app.id = campaign.applicationid
SELECT flag FROM flags where flag.accountid = app.accountid
SET rule.new_column TO
CASE (flag.new = true AND flag.old = false) THEN 'v2'
CASE (flag.new = false AND flag.old = true) THEN 'v1'
ELSE 'v0'
Example:
flags:
accountid | new | old
---------------------
1 |true |true
2 |true |false
applications:
id | accountid
--------------
3 | 1
4 | 2
campaigns:
id | applicationid
------------------
5 | 3
6 | 4
rules:
campaignid | new_column (wanted result)
----------------------
5 | 'v0'
5 | 'v0'
6 | 'v2'

This should do it:
update rules
set new_column = case
when f.new and not f.old then 'v2'
when not f.new and f.old then 'v1'
else 'v0'
end
from campaigns c
join applications a on c.applicationid = a.id
join flags f on a.accountid = f.accountid
where c.id = rules.campaignid;
Online example: http://rextester.com/JHHP6967

I think I've achieved what I wanted with JOINed query expression (thanks #joop).
I'll post it here in case it might be useful for others:
UPDATE
rules AS r
SET
rbversion =
CASE
WHEN f.new = TRUE AND f.old = FALSE THEN 'v2'
WHEN f.new = FALSE AND f.old = TRUE THEN 'v1'
ELSE 'v0'
END
FROM
flags AS f,
campaigns AS c,
applications AS a
WHERE
c.id = r.campaignid AND
a.id = c.applicationid AND
f.accountid = a.accountid

Related

Maintaining order in DB2 "IN" query

This question is based on this one. I'm looking for a solution to that question that works in DB2. Here is the original question:
I have the following table
DROP TABLE IF EXISTS `test`.`foo`;
CREATE TABLE `test`.`foo` (
`id` int(10) unsigned NOT NULL auto_increment,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Then I try to get records based on the primary key
SELECT * FROM foo f where f.id IN (2, 3, 1);
I then get the following result
+----+--------+
| id | name |
+----+--------+
| 1 | first |
| 2 | second |
| 3 | third |
+----+--------+
3 rows in set (0.00 sec)
As one can see, the result is ordered by id. What I'm trying to achieve is to get the results ordered in the sequence I'm providing in the query. Given this example it should return
+----+--------+
| id | name |
+----+--------+
| 2 | second |
| 3 | third |
| 1 | first |
+----+--------+
3 rows in set (0.00 sec)
You could use a derived table with the IDs you want, and the order you want, and then join the table in, something like...
SELECT ...
FROM mcscb.mcs_premise prem
JOIN mcscb.mcs_serv_deliv_id serv
ON prem.prem_nb = serv.prem_nb
AND prem.tech_col_user_id = serv.tech_col_user_id
AND prem.tech_col_version = serv.tech_col_version
JOIN (
SELECT 1, '9486154876' FROM SYSIBM.SYSDUMMY1 UNION ALL
SELECT 2, '9403149581' FROM SYSIBM.SYSDUMMY1 UNION ALL
SELECT 3, '9465828230' FROM SYSIBM.SYSDUMMY1
) B (ORD, ID)
ON serv.serv_deliv_id = B.ID
WHERE serv.tech_col_user_id = 'CRSSJEFF'
AND serv.tech_col_version = '00'
ORDER BY B.ORD
You can use derived column to do custom ordering.
select
case
when serv.SERV_DELIV_ID = '9486154876' then 1 ELSE
when serv.SERV_DELIV_ID = '9403149581' then 2 ELSE 3
END END as custom_order,
...
...
ORDER BY custom_order
To make the logic a little bit more evident you might modify the solution provided by bhamby like so:
WITH ordered_in_list (ord, id) as (
VALUES (1, '9486154876'), (2, '9403149581'), (3, '9465828230')
)
SELECT ...
FROM mcscb.mcs_premise prem
JOIN mcscb.mcs_serv_deliv_id serv
ON prem.prem_nb = serv.prem_nb
AND prem.tech_col_user_id = serv.tech_col_user_id
AND prem.tech_col_version = serv.tech_col_version
JOIN ordered_in_list il
ON serv.serv_deliv_id = il.ID
WHERE serv.tech_col_user_id = 'CRSSJEFF'
AND serv.tech_col_version = '00'
ORDER BY il.ORD

Efficient way to conditionally update values in 2 records in PostgreSQL

I have a table that contains 4 columns
id | category | score | enabled
1 | news | 95 | t
id -- serial
category -- varchar
score -- float
enabled -- bool
I want to update enabled to False if there's another record with a higher score.
For example, if I have:
id | category | score | enabled
1 | news | 95 | t
Then, after some operation, a new record with the same category is inserted:
id | category | score | enabled
1 | news | 95 | t
2 | news | 100 | f
Since the score for id=2 is higher, I want to change enabled for id=2 to True and change enabled for id=1 to False.
I'm wondering if I can combine these operations into 1 query. Right now I do 2 SELECT queries to get the 2 records, then compare the scores locally, and then change the enabled value (if needed).
So simply,
SELECT id, score
FROM table
WHERE category = %s
AND enabled = True
SELECT id, score
FROM table
WHERE category = %s
AND id = (SELECT max(id) WHERE category=%s)
if score2>= score1:
UPDATE table SET enabled = True
WHERE id = id2
UPDATE table SET enabled = False
WHERE id = id1
It works, but it seems very inefficient. Any way to improve these queries?
You can do that with a single update:
update the_table
set enabled = (score = t.max_score)
from (
select id, category, max(score) over (partition by category) as max_score
from the_table
where category = 'news'
) t
where t.id = the_table.id
and t.category = the_table.category;
This will set the enabled flags for all rows with the same category in a single statement.
Online example: https://rextester.com/DXR80618
If you happen to have more than one row with the same highest score for one category, the above statement will change enabled to true for all of, .
E.g.
id | category | score
---+----------+------
1 | news | 95
2 | news | 100
3 | news | 100
If you don't want that, and e.g. always pick the one with the lowest id to be the enabled row, you can use the following:
update the_table
set enabled = (rn = 1)
from (
select id, category,
row_number() over (partition by category order by score desc, id) as rn
from the_table
where category = 'news'
) t
where t.id = the_table.id
and t.category = the_table.category;
Online example: https://rextester.com/JPA61125

Unpack expression results from case statement

Four categories in category table.
id | name
--------------
1 | 'wine'
2 | 'chocolate'
3 | 'autos'
4 | 'real estate'
Two of the many (thousands of) forecasters in forecaster table.
id | name
--------------
1 | 'sothebys'
2 | 'cramer'
Relevant forecasts by the forecasters for the categories in the forecast table.
| id | forecaster_id | category_id | forecast |
|----+---------------+-------------+--------------------------------------------------------------|
| 1 | 1 | 1 | 'bad weather, prices rise short-term' |
| 2 | 1 | 2 | 'cocoa bean surplus, prices drop' |
| 3 | 1 | 3 | 'we dont deal with autos - no idea' |
| 4 | 2 | 2 | 'sell, sell, sell' |
| 5 | 2 | 3 | 'demand for cocoa will skyrocket - prices up - buy, buy buy' |
I want prioritized mapping of (forecaster, category, forecast) such that, if a forecast exists for some primary forecaster (e.g. 'cramer') use it because I trust him more. If a forecast exists for some secondary forecaster (e.g. 'sothebys') use that. If no forecast exists for a category, return a row with that category and null for forecast.
I have something that almost works and after I get the logic down I hope to turn into parameterized query.
select
case when F1.category is not null
then (F1.forecaster, F1.category, F1.forecast)
when F2.category is not null
then (F2.forecaster, F2.category, F2.forecast)
else (null, C.category, null)
end
from
(
select
FR.name as forecaster,
C.id as cid,
C.category as category,
F.forecast
from
forecast F
inner join forecaster FR on (F.forecaster_id = FR.id)
inner join category C on (C.id = F.category_id)
where FR.name = 'cramer'
) F1
right join (
select
FR.name as forecaster,
C.id as cid,
C.category as category,
F.forecast
from
forecast F
inner join forecaster FR on (F.forecaster_id = FR.id)
inner join category C on (C.id = F.category_id)
where FR.name = 'sothebys'
) F2 on (F1.cid = F2.cid)
full outer join category C on (C.id = F2.cid);
This gives:
'(sothebys,wine,"bad weather, prices rise short-term")'
'(cramer,chocolate,"sell, sell, sell")'
'(cramer,autos,"demand for cocoa will skyrocket - prices up - buy, buy buy")'
'(,"real estate",)'
While that is the desired data it is a record of one column instead of three. The case was the only way I could find to achieve the ordering of cramer first sothebys next and there is lots of duplication. Is there a better way and how can the tuple like results be pulled back apart into columns?
Any suggestions, especially related to removal of duplication or general simplification appreciated.
This sounds like a case for DISTINCT ON (untested):
SELECT DISTINCT ON (c.id)
fr.name AS forecaster,
c.name AS category,
f.forecast
FROM forecast f
JOIN forecaster fr ON f.forecaster_id = fr.id
RIGHT JOIN category c ON f.category_id = c.id
ORDER BY
c.id,
CASE WHEN fr.name = 'cramer' THEN 0
WHEN fr.name = 'sothebys' THEN 1
ELSE 2
END;
For each category, the first row in the ordering will be picked. Since Cramer has a higher id than Sotheby's, it will be given preference.
Adapt the ORDER BY clause if you need a more complicated ranking.

EAV data in SQL Server

I have no control over the data or the database structure. I have this EAV type of data where a consultant can speak one or many languages and he can travel to 1 or many countries in Europe and he has many skills indeed.
FYI there are 10 different main categories in my data.
Some consultants speak 10 languages while other speak only one.
The data looks a bit like this
____________________________________________
| ConsultantID | Category | Value |
--------------------------------------------
| 1 | Language | English |
| 1 | Language | French (fluent) |
| 1 | Language | Spanish (working)|
| 1 | Country | Ireland |
| 1 | Country | Italy |
| 1 | Country | Germany |
| 1 | Country | Belgium |
| 456 | Language | French (working) |
| 456 | Country | Belgium |
| 847 | Language | English |
| 847 | Country | Belgium |
--------------------------------------------
I want to list all consultants willing to travel to Belgium and who speak French (working or fluent). Based on my current example that would be #1 and #456
I wrote the query below which list all values matching a category for a consultant (note this is not dynamic as the number of value in my example is set to 5 max - so already a poor design).
SELECT
ID, category,
MAX(CASE seq WHEN 1 THEN value ELSE '' END ) +
MAX(CASE seq WHEN 2 THEN ',' + value ELSE '' END ) +
MAX(CASE seq WHEN 3 THEN ',' + value ELSE '' END ) +
MAX(CASE seq WHEN 4 THEN ',' + value ELSE '' END ) +
MAX(CASE seq WHEN 5 THEN ',' + value ELSE '' END )
FROM
(SELECT
p1.ID, p1.category, p1.value,
(SELECT COUNT(*)
FROM tblWebPracticeInfo p2
WHERE p2.category = p1.category
AND p2.ID = P1.ID
AND p2.value <= p1.value)
FROM
tblWebPracticeInfo p1) D (ID, category, value, seq )
GROUP BY
ID, category
ORDER BY
ID;
I would then need to query this table...
But without even a where clause it takes already 2 seconds to execute
I have something else more basic (but similarly not efficient)
select *
from tblWebMemberInfo m
where
m.ID in (select p.id from tblWebPracticeInfo p
where p.category = 'Language' and p.value like 'French%')
and m.ID in (select p.id from tblWebPracticeInfo p
where p.category = 'Country' and p.value = 'Belgium')
order by m.ID
That's basically where I am. As you can see nothing genius and nothing which is really working.
Can you point me to the right track.
I'm using SQL Server 2005 - v9.00.1
Many thanks in advance for your time & help
If you just need to list the consultants then you can use exists():
select p.Id ...
from Person p /* Assuming you have a regular table for people,
if not, use distinct or group by */
where exists (
select 1
from tblWebPracticeInfo l
where l.Id = p.Id
and l.Category = 'Language'
and l.Value = 'French'
)
and exists (
select 1
from tblWebPracticeInfo c
where c.Id = p.Id
and c.Category = 'Country'
and c.Value = 'Belgium'
)
You could also use aggregation and having like so:
select ConsultantID
from tblWebMemberInfo m
where (p.category = 'Language' and p.value like 'French%')
or (p.category = 'Country' and p.value = 'Belgium')
group by ConsultantID
having count(*) = 2 /* number of conditions to match is 2 */

postgres select result

if the data in table is :
select * from data;
key | value
------+---------
a | a_value
b | b_value
c | c_value
d | d_value
e | e_value
f | f_value
if one client connects to my web server and ask for the value of a, my application will try to do:
select value where key = 'c';
and send the result value to client
if 3 clients connect to my web server, there are 3 statements occur,
select value where key = 'e'; # client 1
select value where key = 'c'; # client 2
select value where key = 'a'; # client 3
for resource reason, I want to consolidate many statements into one select statement.
select * where key = 'e' or key = 'c' or key = 'a';
key | value
------+---------
a | a_value
c | c_value
e | e_value
but the problem is the sequence of result is not the same as my WHERE clause.
so I cannot distinguish the result from the clients' query.
if I want to send value back to the clients
'e_value' to client 1
'c_value' to client 2
'a_value' to client 3
thanks!!
I don't quite understand your use-case, but a query like the following enables you to
.. get results in pre-defined order
.. identify rows in result set
SELECT v.*, d.value
FROM data
JOIN (
VALUES
(1::int, 'e'::text) -- explicit cast may not be necessary
,(2, 'c')
,(3, 'a')
) v(client, key) USING (key)
ORDER BY v.client;
Returns:
client | key | value
-------+-----+------
1 | e | e_value
2 | c | c_value
3 | a | a_value