I need to write a query (or function) that will update existing records in a table from with values stored in an hstore column in another table. For example:
create temp table foo(id int primary key, f1 int, f2 text, f3 int);
insert into foo values
(1, 1, 'jack', 1),
(2, 2, 'ted' , 2),
(3, 3, 'fred', 3);
create temp table bar(foo_id int references foo(id), row_data hstore);
insert into bar values
(1, 'f1=>0, f2=>bill'::hstore),
(2, 'f1=>0, f2=>will, f3=>0'::hstore),
(3, 'f3=>0'::hstore);
Only columns that have values in the hstore column should get updated, so after processing, the desired result would be:
select * from foo;
+----+----+------+----+
| id | f1 | f2 | f3 |
+----+----+------+----+
| 1 | 0 | bill | 1 |
| 2 | 0 | will | 0 |
| 3 | 3 | fred | 0 |
+----+----+------+----+
What is the "best" way to update foo with the values in bar?
Note: I'm defining best as being the easiest to code. While performance is always important, this is a batch job and the speed is not as critical as it might be if a user was waiting on the results.
I'm using PostgreSQL 9.4.
To retain original column values if nothing is supplied in the hstore column ...
Simple method with COALESCE
UPDATE foo f
SET f1 = COALESCE((b.row_data->'f1')::int, f1)
, f2 = COALESCE( b.row_data->'f2' , f2)
, f3 = COALESCE((b.row_data->'f3')::int, f3)
FROM bar b
WHERE f.id = b.foo_id
AND b.row_data ?| '{f1,f2,f3}'::text[];
The added last line excludes unaffected rows from the UPDATE right away: the ?| operator checks (per documentation):
does hstore contain any of the specified keys?
If that's not the case it's cheapest not to touch the row at all.
Else, at least one (but not necessarily all!) of the columns receives an UPDATE. That's where COALESCE comes in.
However, per documentation:
A value (but not a key) can be an SQL NULL.
So COALESCE cannot distinguish between two possible meanings of NULL here:
The key 'f2'` was not found.
b.row_data->'f2' returns NULL as new value for f2.
Works for NULL values, too
UPDATE foo f
SET f1 = CASE WHEN b.row_data ? 'f1'
THEN (b.row_data->'f1')::int ELSE f1 END
, f2 = CASE WHEN b.row_data ? 'f2'
THEN b.row_data->'f2' ELSE f2 END
, f3 = CASE WHEN b.row_data ? 'f3'
THEN (b.row_data->'f3')::int ELSE f3 END
FROM bar b
WHERE f.id = b.foo_id
AND b.row_data ?| '{f1,f2,f3}'::text[];
The ? operator checks for a single key:
does hstore contain key?
So you're after a simple update? As f1 and f3 are integers you need to cast those. Otherwise it's just:
UPDATE foo SET f1 = (row_data->'f1')::integer,
f2 = row_data->'f2',
f3 = (row_data->'f3')::integer
FROM bar WHERE foo.id = foo_id;
Related
I have table like this :
ID | key | value
1 | A1 |o1
1 | A2 |o2
1 | A3 |o3
2 | A1 |o4
2 | A2 |o5
3 | A1 |o6
3 | A3 |o7
4 | A3 |o8
I want to write a oracle query that can filter value column based on key column .
some thing like this
select ID
where
if key = A1 then value ='o1'
and key = A3 then value ='o4'
please help me to write this query.
***To clarify my question ,I need list of IDs in result that all condition(key-value) are true for them. for each IDs I should check key-values (with AND ) and if all conditions are true then this ID is acceptable .
thanks
IF means PL/SQL. In SQL, we use CASE expression instead (or DECODE, if you want). Doing so, you'd move value out of the expression and use something like this:
where id = 1
and value = case when key = 'A1' then 'o1'
when key = 'A3' then 'o4'
end
You are mixing filtering and selection. List the columns that you want to display in the SELECT list and the columns used to filter in the WHERE clause
SELECT key, value
FROM my_table
WHERE ID = 1 AND key IN ('A1', 'A2')
If there is no value column in your table, you can use the DECODE function
SELECT key, DECODE(key, 'A1', 'o1', 'A2', 'o4', key) AS value
FROM my_table
WHERE ID = 1
After the key, you must specify pairs of search and result values. The pairs can be followed by a default value. In this example, since we did not specify a result for 'A3', the result will be the key itself. If no default value was specified, NULL would be returned for missing search values.
update
It seems that I have misunderstood the question (see #mathguy's comment). You can filter the way you want by simply using the Boolean operators AND and OR
SELECT * FROM
FROM my_table
WHERE
ID = 1 AND
(
key = 'A1' AND value ='o1' OR
key = 'A3' AND value ='o4'
)
By using this pattern it is easy to add more constraints of this kind. Note that AND has precedence over OR (like * over +).
I have the following two queries:
Query #1
(SELECT ARRAY (SELECT (journeys.id)
FROM JOURNEYS
JOIN RESPONSES ON scenarios[1] = responses.id) AS arry);
This one returns an array.
Query #2:
SELECT (journeys_index.j_index)
FROM journeys_index
WHERE environment = 'env1'
AND for_channel = 'ch1'
AND first_name = 'name1';
This second query returns the element index in the former array.
How do I combine the two to get only the element value?
I recreated a simpler example with a table containing an array column (the result of your first query)
create table my_array_test (id int, tst_array varchar[]);
insert into my_array_test values (1,'{cat, mouse, frog}');
insert into my_array_test values (2,'{horse, crocodile, rabbit}');
And another table containing the element position for each row I want to extract.
create table my_array_pos_test (id int, pos int);
insert into my_array_pos_test values (1,1);
insert into my_array_pos_test values (2,3);
e.g. from the row in my_array_test with id=1 I want to extract the 1st item (pos=1) and from the row in my_array_test with id=2 I want to extract the 3rd item (pos=3)
defaultdb=> select * from my_array_pos_test;
id | pos
----+-----
1 | 1
2 | 3
(2 rows)
Now the resulting statement is
select *,
tst_array[my_array_pos_test.pos]
from
my_array_test join
my_array_pos_test on my_array_test.id = my_array_pos_test.id
with the expected result
id | tst_array | id | pos | tst_array
----+--------------------------+----+-----+-----------
1 | {cat,mouse,frog} | 1 | 1 | cat
2 | {horse,crocodile,rabbit} | 2 | 3 | rabbit
(2 rows)
Now, in your case I would probably do something similar to the below, assuming your 1st select statement returns one row only.
with array_sel as
(SELECT ARRAY (SELECT (journeys.id)
FROM JOURNEYS
JOIN RESPONSES ON scenarios[1] = responses.id) AS arry)
SELECT arry[journeys_index.j_index]
FROM journeys_index cross join array_sel
WHERE environment = 'env1'
AND for_channel = 'ch1'
AND first_name = 'name1';
I can't validate fully the above sql statement since we can't replicate your tables, but should give you a hint on where to start from
Suppose I have a PostgreSQL table t that looks like
id | name | y
----+------+---
0 | 'a' | 0
1 | 'b' | 0
2 | 'c' | 0
3 | 'd' | 1
4 | 'e' | 2
5 | 'f' | 2
With id being the primary key and with a UNIQUE constraint on (name, y).
Suppose I want to update this table in such a way that the part of the data set with y = 0 becomes (without knowing what is already there)
id | name | y
----+------+---
0 | 'a' | 0
1 | 'x' | 0
2 | 'y' | 0
I could use
DELETE FROM t WHERE y = 0 AND name NOT IN ('a', 'x', 'y');
INSERT INTO t (name, y) VALUES ('a', 0), ('x', 0), ('y', 0)
ON CONFLICT (name) DO NOTHING;
I feel like there must be a one-statement way to do this (like what upsert does for the task "update the existing entries and insert missing ones", but then for "insert the missing entries and delete the entries that should not be there"). Is there? I heard rumours that oracle has something called MERGE... I'm not sure what it does exactly.
This can be done with a single statement. But I doubt whether that classifies as "simpler".
Additionally: your expected output doesn't make sense.
Your insert statement does not provide a value for the primary key column (id), so apparently, the id column is a generated (identity/serial) column.
But in that case, news rows can't have the same IDs as the ones before because when the new rows were inserted, new IDs were generated.
Given the above change to your expected output, the following does what you want:
with data (name, y) as (
values ('a', 0), ('x', 0), ('y', 0)
), changed as (
insert into t (name, y)
select *
from data
on conflict (name,y) do nothing
)
delete from t
where (name, y) not in (select name, y from data);
That is one statement, but certainly not "simpler". The only advantage I can see is that you do not have to specify the list of values twice.
Online example: https://rextester.com/KKB30299
Unless there's a tremendous number of rows to be updated, do it as three update statements.
update t set name = 'a' where id = 0;
update t set name = 'x' where id = 1;
update t set name = 'y' where id = 2;
This is simple. It's easily done in a loop with a SQL builder. There's no race conditions as there are with deleting and inserting. And it preserves the ids and other columns of those rows.
To demonstrate with some psuedo-Ruby code.
new_names = ['a', 'x', 'y']
# In a transaction
db.transaction {
# Query the matching IDs in the same order as their new names
ids_to_update = db.select("
select id from t where y = 0 order by id
")
# Iterate through the IDs and new names together
ids_to_update.zip(new_names).each { |id,name|
# Update the row with its new name
db.execute("
update t set name = ? where id = ?
", name, id)
}
}
Fooling around some, here's how I did it in "one" statement, or at least one thing sent to the server, while preserving the IDs and no race conditions.
do $$
declare change text[];
declare changes text[][];
begin
select array_agg(array[id::text,name])
into changes
from unnest(
(select array_agg(id order by id) from t where y = 0),
array['a','y','z']
) with ordinality as a(id, name);
foreach change slice 1 in array changes
loop
update t set name = change[2] where id = change[1]::int;
end loop;
end$$;
The goal is to produce an array of arrays matching the id to its new name. That can be iterated over to do the updates.
unnest(
(select array_agg(id order by id) from t where y = 0),
array['a','y','z']
) with ordinality as a(id, name);
That bit produces rows with the IDs and their new names side by side.
select array_agg(array[id::text,name])
into changes
from unnest(...) with ordinality as a(id, name);
Then those rows of IDs and names are turned into an array of arrays like: {{1,a},{2,y},{3,z}}. (There's probably a more direct way to do that)
foreach change slice 1 in array changes
loop
update t set name = change[2] where id = change[1]::int;
end loop;
Finally we loop over the array and use it to perform each update.
You can turn this into a proper function and pass in the y value to match and the array of names to change them to. You should verify that the length of the ids and names match.
This might be faster, depends on how many rows you're updating, but it isn't simpler, and it took some time to puzzle out.
I'd like to expand a question I posted a while ago:
I'm querying a table for rows in which pairs of columns are in a specific set. For example, consider the following table:
id | f1 | f2
-------------
1 | 'a' | 20
2 | 'b' | 20
3 | 'a' | 30
4 | 'b' | 20
5 | 'c' | 20
And I wish to extract rows in which the pair (f1, f2) are in a specified set of pairs, e.g. (('a',30), ('b', 20),...). In the original question, Mark answered correctly that I can use the following syntax:
SELECT * FROM my_table WHERE (f1,f2) IN (('a',30), ('b',20))
This works fine, but I see some unexpected behavior regarding indexes:
I've defined a multi-column index for f1, f2, named IndexF1F2. Using the EXPLAIN phrase, I see that MySql uses the index for a single comparison, e.g.:
SELECT * FROM my_table WHERE (f1,f2) = ('a',30)
but not when using the 'IN' clause, as in the example above. Giving hints, e.g. USE INDEX(IndexF1F2) or even FORCE INDEX(IndexF1F2), does not seem to make any difference.
Any ideas?
This is a known bug in MySQL.
Use this syntax:
SELECT *
FROM composite
WHERE (f1, f2) = ('a', 30)
OR (f1, f2) = ('b', 20)
I am trying to write a view that has 3 columns: Planet, Moon, and Largest.
The view is meant to show planets, their moons, and a Yes or No column indicating whether or not it is the largest moon for the planet.
Only one Basetable is used, and the columns I am referencing are moonPlanetOrbit (only not null if bodyType is = to 'Moon'), bodyName (name of the moon), and largest ('yes' or 'no').
Here is my attempt so far:
CREATE VIEW Moons (Planet, Moon, Largest)
select moonPlanetOrbited, bodyName, ('Yes' if bodyName = (SELECT top 1 moonMeanRadius from Body where moonPlanetOrbited = bodyName order by moonMeanRadius) as Largest)
I can provide any more information if needed.
Thanks,
Cody
SQL works best with sets of data. My advice is to get the set of largest moons using a SELECT statement and the MAX() function, and then join the result set with the whole table. Then test whether the moon is equal to the largest in order to print 'yes' or 'no'.
Here's an example using MySQL. I created a table Moons containing the columns moonPlanetOrbited, bodyName, moonMeanRadius. The following SQL selects the largest moonMeanRadius for a given moonPlanetOrbited:
SELECT moonPlantedOrbited, MAX(moonMeanRadius) as maxMoonRadius
FROM Moons
GROUP BY moonPlanetOrbitede
Now that we have a list of maxMoonRadius, join the result set with the entire table and test if the moonMeanRadius is equal to the maxMoonRadius:
SELECT m1.moonPlanetOrbited, m2.bodyName,
if(m1.moonMeanRadius = m2.maxMoonRadius, 'Yes', 'No') as Largest
FROM Moons m1
JOIN (
SELECT moonPlanetOrbited, MAX(moonMeanRadius) as maxMoonRadius
FROM Moons
GROUP BY moonPlanetOrbited
) m2
ON m1.moonPlanetOrbited = m2.moonPlanetOrbited;
The IF syntax is from MySQL 5.5:
http://dev.mysql.com/doc/refman/5.5/en/control-flow-functions.html#function_if
Tested using the following SQL :
CREATE TABLE Moons(
moonPlanetOrbited VARCHAR(255),
bodyName VARCHAR(255),
moonMeanRadius FLOAT
);
INSERT INTO Moons('a', 'b', 1.01);
INSERT INTO Moons('a', 'c', 1.02);
INSERT INTO Moons('a', 'd', 1.03);
INSERT INTO Moons('a', 'e', 1.04);
+-------------------+----------+---------+
| moonPlanetOrbited | bodyName | Largest |
+-------------------+----------+---------+
| a | b | No |
| a | c | No |
| a | d | No |
| a | e | Yes |
+-------------------+----------+---------+
4 rows in set (0.00 sec)
Here is my MS-SQL Syntax stab at it:
SELECT
B.moonPlanetOrbited
, B.bodyName
, CASE
WHEN B.bodyName =
(SELECT TOP 1
iB.bodyName
FROM
Body AS iB
WHERE
iB.moonPlanetOrbited = B.bodyName
ORDER BY
iB.moonMeanRadius DESC
)
THEN 'Yes'
ELSE 'No'
END CASE AS [Largest]
FROM
Body AS B
If the table uses IDs as a primary key it may be better to compare the IDs instead of the names.
Here is an attempt (untested) that resembles your approach as closely as possible, since your idea wasn't that far off:
Select
M.moonPlanetOrbited,
M.bodyName,
CASE
WHEN M.bodyName =
(SELECT top 1 bodyName from Body
where moonPlanetOrbited = M.moonPlanetOrbited
order by moonMeanRadius DESC)
Then 'Y'
Else 'N'
AS Largest
FROM body
You just needed a table prefix to actually do the correlating to the root table, and also to make sure that you were comparing apples to apples in your CASE statement.