Postgres: Update JSON Array and remove element matched with key - postgresql

Given the table below
drop table if exists documents;
create table documents(docu_id text, attachments jsonb);
insert into documents values
('001',
'[{"province":"test province","city":"test city","barangay":"test barangay,"street":"test ST"},
{"province":"test province 2","city":"test city 2","barangay":"test barangay 2","street":"test street 2"}]'
),
('002',
'[{"province":"test province 2 1":"VALENZUELA CITY","test barangay 2 1":"test barangay 2 1","street":"test street 2 1"},
{"province":"test province 2 2","city":"test city 2 2","barangay":"test barangay 2 2","street":"test strett 2 2"}]'
);
How can i update json array matching street key word: test ST
expected output
[{"province":"test province 2","city":"test city 2","barangay":"test barangay 2","street":"test street 2"}]
[{"province":"test province 2 1":"VALENZUELA CITY","test barangay 2 1":"test barangay 2 1","street":"test street 2 1"},
{"province":"test province 2 2","city":"test city 2 2","barangay":"test barangay 2 2","street":"test strett 2 2"}]

demo:db<>fiddle
SELECT
docu_id,
jsonb_agg(elements.value) -- 3
FROM
documents,
jsonb_array_elements(attachments) AS elements -- 1
WHERE elements ->> 'street' != 'test ST' -- 2
GROUP BY docu_id -- 3
Expand array elements into one row each with jsonb_array_elements()
Filter these elements by whatever you like
Group remaining elements again into a JSON array using aggregate function jsonb_agg()
Update:
UPDATE documents d
SET attachments = s.new_attachments
FROM (
SELECT
docu_id,
jsonb_agg(elements.value) AS new_attachments
FROM
documents,
jsonb_array_elements(attachments) AS elements
WHERE elements.value ->> 'street' != 'test ST'
GROUP BY docu_id
) s
WHERE d.docu_id = s.docu_id
demo:db<>fiddle

Updating remaining value from json matched data instead no update happened. it will update as null or empty json.
UPDATE street_lov SET street = sub
FROM COALESCE((SELECT jsonb_agg(elements.value) as elements FROM street_lov,
jsonb_array_elements(street::jsonb) AS elements
WHERE elements.value ->> 'street' != 'test ST'
AND id = (SELECT id FROM street_id)
GROUP BY id ), '[]') sub where street_lov.id = (select id from street_id)
RETURNING street_lov.id,street

Related

PostgreSQL: Merging sets of rows which text fields are contained in other sets of rows

Given the following table, I need to merge the fields in different "id" only if they are the same type (person or dog), and always as the value of every field of an "id" is contained in the values of other "ids".
id
being
feature
values
1
person
name
John;Paul
1
person
surname
Smith
2
dog
name
Ringo
3
dog
name
Snowy
4
person
name
John
4
person
surname
5
person
name
John;Ringo
5
person
surname
Smith
In this example, the merge results should be as follows:
1 and 4 (Since 4's name is present in 1's name and 4's surname is empty)
1 and 5 cannot be merged (the name field show different values)
4 and 5 can be merged
2 and 3 (dogs) cannot be merged. They have only the field "name" and they do not share values.
2 and 3 cannot be merged with 1, 4, 5 since they have different values in "being".
id
being
feature
values
1
person
name
John;Paul
1
person
surname
Smith
2
dog
name
Ringo
3
dog
name
Snowy
5
person
name
John;Ringo
5
person
surname
Smith
I have tried this:
UPDATE table a
SET values = (SELECT array_to_string(array_agg(distinct values),';') AS values FROM table b
WHERE a.being= b.being
AND a.feature= b.feature
AND a.id<> b.id
AND a.values LIKE '%'||a.values||'%'
)
WHERE (select count (*) FROM (SELECT DISTINCT c.being, c.id from table c where a.being=c.being) as temp) >1
;
This doesn't work well because it will merge, for example, 1 and 5. Besides, it duplicates values when merging that field.
One option is to aggregate names with surnames on "id" and "being". Once you get a single string per "id", a self join may find when a full name is completely included inside another (where the "being" is same for both "id"s), then you just select the smallest fullname, candidate for deletion:
WITH cte AS (
SELECT id,
being,
STRING_AGG(values, ';') AS fullname
FROM tab
GROUP BY id,
being
)
DELETE FROM tab
WHERE id IN (SELECT t2.id
FROM cte t1
INNER JOIN cte t2
ON t1.being = t2.being
AND t1.id > t2.id
AND t1.fullname LIKE CONCAT('%',t2.fullname,'%'));
Check the demo here.

UPDATE statement using two arrays at the same index in WHERE clause

I am trying to update a table, entities with a column, contacts that is an array of ids from another table, contacts. The contacts table has the columns first_name and last_name, and I have an array of first names, firstNames and last names, lastNames to pass in.
How would you update the contacts column in the entities table with one query that properly gets all of the contacts with first name firstNames[0] AND last name lastNames[0], and all of the contacts with first name firstNames[1] AND last name lastNames[1], and [...] all of the contacts with first name firstNames[n] AND last name lastNames[n]?
My initial thought was something like UPDATE entities SET contacts = (SELECT id FROM contacts WHERE first_name = ANY(firstNames) AND last_name = ANY(lastNames).
The problem with this arrises when the contacts table is like this:
first_name | last_name
----------------------
Bob | Jones
Bob | Miller
David | Miller
If I wanted to set the contacts column to the Ids for Bob Jones and David Miller, but NOT Bob Miller, and I passed in ['Bob', 'David'] for firstNames and ['Jones', 'Miller'] for lastNames in the above query, Bob Miller would also get added to the contacts column.
May be you look for something like this:
WITH x AS (
SELECT 'Bob'::text AS firstName, 'Jones'::text AS lastName
UNION SELECT 'David', 'Miller'
UNION SELECT 'Bob', 'Miller'
)
SELECT *
FROM x
WHERE (firstName, lastName) = ANY (ARRAY [
('Bob'::text, 'Jones'::text),
('David'::text, 'Miller'::text)
]);
Yet another way:
WITH x AS (
SELECT 'Bob'::text AS firstName, 'Jones'::text AS lastName
UNION SELECT 'David', 'Miller'
UNION SELECT 'Bob', 'Miller'
)
SELECT *
FROM x
WHERE EXISTS (
SELECT 1
FROM (SELECT ARRAY [
['Bob', 'Jones'],
['David', 'Miller']]::text[][] AS n
) AS n
JOIN LATERAL generate_series(1, array_upper(n, 1)) AS i ON true
WHERE firstName = n[i][1]
AND lastName = n[i][2]
);

Using a WHERE EXISTS statement for two contradictory conditions in Redshift/Postgres?

I'm looking to write a query that, given an id, first name, and last name, returns IDs corresponding to every ID that has at least one row containing a first name 'Steve' and a last name 'Smith', in addition to at least one row that corresponds to a first name 'Steve' and does not correspond to a last name 'Smith'. I tried the below query but it returns 0 rows.
SELECT DISTINCT id FROM t
WHERE EXISTS (SELECT 1 FROM t WHERE first_name = 'Steve' AND last_name != 'Smith')
AND NOT EXISTS (SELECT 1 FROM t WHERE first_name = 'Steve' AND last_name = 'Smith')
I suspect it's because within a single row, both conditions cannot simultaneously be true, even though they can both be true across multiple rows for the same ID.
How should I modify or rewrite this query to return the IDs of interest?
Apparently you could write this another way. Give me all IDs where there's at least one Steve Smith but not all of them are Steve Smiths.
select id
from t
group by id
having count(case when first_name = 'Steve' and last_name = 'Smith' then 1 end)
between 1 and count(last_name) - 1

Find all records NOT in any blocked range where blocked ranges are in a table

I have a table TaggedData with the following fields and data
ID GroupID Tag MyData
** ******* *** ******
1 Texas AA01 Peanut Butter
2 Texas AA15 Cereal
3 Ohio AA05 Potato Chips
4 Texas AA08 Bread
I have a second table of BlockedTags as follows:
ID StartTag EndTag
** ******** ******
1 AA00 AA04
2 AA15 AA15
How do I select from this to return all data matching a given GroupId but NOT in any blocked range (inclusive)? For the data given if the GroupId is Texas, I don't want to return Cereal because it matches the second range. It should only return Bread.
I did try left joins based queries but I'm not even that close.
Thanks
create table TaggedData (
ID int,
GroupID varchar(16),
Tag char(4),
MyData varchar(50))
create table BlockedTags (
ID int,
StartTag char(4),
EndTag char(4)
)
insert into TaggedData(ID, GroupID, Tag, MyData)
values (1, 'Texas', 'AA01', 'Peanut Butter')
insert into TaggedData(ID, GroupID, Tag, MyData)
values (2, 'Texas' , 'AA15', 'Cereal')
insert into TaggedData(ID, GroupID, Tag, MyData)
values (3, 'Ohio ', 'AA05', 'Potato Chips')
insert into TaggedData(ID, GroupID, Tag, MyData)
values (4, 'Texas', 'AA08', 'Bread')
insert into BlockedTags(ID, StartTag, EndTag)
values (1, 'AA00', 'AA04')
insert into BlockedTags(ID, StartTag, EndTag)
values (2, 'AA15', 'AA15')
select t.* from TaggedData t
left join BlockedTags b on t.Tag between b.StartTag and b.EndTag
where b.ID is null
Returns:
ID GroupID Tag MyData
----------- ---------------- ---- --------------------------------------------------
3 Ohio AA05 Potato Chips
4 Texas AA08 Bread
(2 row(s) affected)
So, to match on given GroupID you change the query like that:
select t.* from TaggedData t
left join BlockedTags b on t.Tag between b.StartTag and b.EndTag
where b.ID is null and t.GroupID=#GivenGroupID
I Prefer the NOT EXISTS simply because it gives you more readability, usability and better performance usually in large data (several cases get better execution plans):
would be like this:
SELECT * from TaggedData
WHERE GroupID=#GivenGroupID
AND NOT EXISTS(SELECT 1 FROM BlockedTags WHERE Tag BETWEEN StartTag ANDEndTag)

Loop with inner loop and split

I have records like this in a table called "Entry":
TABLE: Entry
ID Tags
--- ------------------------------------------------------
1 Coffee, Tea, Cake, BBQ
2 Soda, Lemonade
...etc.
TABLE: Tags
ID TagName
---- -----------
1 Coffee
2 Tea
3 Soda
...
TABLE: TagEntry
ID TAGID ENTRYID
--- ----- -------
1 1 1
2 2 1
3 3 2
....
I need to loop through each record in the entire table for Entry, then for each row loop the comma delimited tags because I need to split each tag then do a Tag lookup based on tag name to grab the TagID, and then ultimately insert TagID, EntryID in a bridge table called TagEntry for each comma delimited tag
Not sure how to go about this.
Try this
;with entry as
(
select 1 id, 'Coffee, Tea, Cake, BBQ' tags
Union all
select 2, 'Soda, Lemonade'
), tags as
(
select 1 id,'Coffee' TagName union all
select 2,'Tea' union all
select 3,'Soda'
), entryxml as
(
SELECT id, ltrim(rtrim(r.value('.','VARCHAR(MAX)'))) as Item from (
select id, CONVERT(XML, N'<root><r>' + REPLACE(tags,',','</r><r>') + '</r></root>') as XmlString
from entry ) x
CROSS APPLY x.XmlString.nodes('//root/r') AS RECORDS(r)
)
select e.id EntryId, t.id TagId from entryxml e
inner join tags t on e.Item = t.TagName
This SQL will split your Entry table, for joining to the others:
with raw as (
select * from ( values
(1, 'Coffee, Tea, Cake, BBQ'),
(2, 'Soda, Lemonade')
) Entry(ID,Tags)
)
, data as (
select ID, Tag = convert(varchar(255),' '), Tags, [Length] = len(Tags) from raw
union all
select
ID = ID,
Tag = case when charindex(',',Tags) = 0 then Tags else convert(varchar(255), substring(Tags, 1, charindex(',',Tags)-1) ) end,
Tags = substring(Tags, charindex(',',Tags)+1, 255),
[Length] = [Length] - case when charindex(',',Tags) = 0 then len(Tags) else charindex(',',Tags) end
from data
where [Length] > 0
)
select ID, Tag = ltrim(Tag)
from data
where Tag <> ''
and returns this for the given input:
ID Tag
----------- ------------
2 Soda
2 Lemonade
1 Coffee
1 Tea
1 Cake
1 BBQ