PostgreSQL trigger which updates count in another table - sql

I am rather new to PostgreSQL and I am really stuck with an apparently simple task. I have a table with music albums and a table with bands features, including how many albums such band released in a specific time, let say between 1990 and 1995. So I need to create a trigger that keeps the bands tabe updated whenever I insert, delete or updaste the albums table.
These are the tables:
Album
id_album id_band year
1 1 1995
2 1 1985
3 2 1993
4 3 1998
Band
id_band num_albums9095
1 1
2 1
3 0
So I have created the following function and trigger:
CREATE FUNCTION update_num_nineties()
RETURNS trigger AS $$ BEGIN
UPDATE band
SET num_albums_eighties = (SELECT COUNT (*) FROM album
where album.year between 1990 and 1995
GROUP BY album.id_band);
END;
$$LANGUAGE plpgsql;
CREATE TRIGGER update_nineties_album_mod_album AFTER UPDATE ON album FOR EACH row EXECUTE PROCEDURE update_num_nineties();
But I would get a subquery used as an expression returned more than one row message anytime I try to update any value to test it.
Would anyone be so kind to help me see why I am goind in the wrong direction?

You need to correlate the subquery with the outer query:
update band b
set num_albums_eighties = (
select count (*)
from album a
where a.year between 1990 and 1995 and a.id_band = b.id_band
) --^-- correlation --^--
While this technically works, it is still rather inefficient, because it resets the whole table when just one row in modified. You can restrict the rows with a where clause:
update band b
set num_albums_eighties = (
select count (*)
from album a
where a.year between 1990 and 1995 and a.id_band = b.id_band
)
where b.id_band in (old.band_id, new.band_id)

Related

Add new column to a table with a value group by value

I have a Circus table as follow
circus_id
circus_date
circus_show_price
1
09-12-2020
78
2
12-01-2021
82
and a Ticket table as follow
ticket_id
circus_id
ticket_category
1
1
Adult
2
1
Student
3
1
Children
4
2
Adult
5
2
Children
6
2
Adult
and i want to alter the circus table by adding a new column called ticket_sold and the value should be as follow
circus_id
circus_date
circus_show_price
ticket_sold
1
09-12-2020
78
3
2
12-01-2021
82
3
this is what I have tried
alter table circus add ticket_sold numeric(3) default 0;
update circus set ticket_sold = (select count(ticket_id) from ticket group by circus_id);
it gives me an error said
single-row subquery returns more than one row
In-general, don't, as you will end up with a ticket_sold column that rapidly becomes out-of-sync with the ticket table.
If you want to have a dynamically updating column then:
1. Use a view.
You can just compute the value whenever you need it:
CREATE VIEW circus_view (circus_id, circus_date, circus_show_price, tickets_sold) AS
SELECT c.circus_id,
c.circus_date,
c.circus_show_price,
(SELECT COUNT(*) FROM ticket t WHERE t.circus_id = c.circus_id)
FROM circus c;
2. Use a trigger.
If you must persist the number of tickets in the circus table then:
ALTER TABLE Circus ADD tickets_sold NUMBER;
CREATE TRIGGER circus_tickets
AFTER INSERT OR UPDATE OR DELETE ON Ticket
BEGIN
UPDATE Circus c
SET tickets_sold = (SELECT COUNT(*) FROM ticket t WHERE t.circus_id = c.circus_id);
END;
/
fiddle
Is is not group by clause you need because query then returns number of tickets per each circus, but - then you get as many rows as there are circus_ids in the ticket table. Instead, correlate subquery to the main table:
update circus c set
c.ticket_sold = (select count(t.ticket_id)
from ticket t
where t.circus_id = c.circus_id
);

Can I get duplicate results (from one table) in an INTERSECT operation between two tables?

I know the wording of the question is awkward, but I couldn't phrase it any better. Let me explain the situation.
There's table A which has a bunch of columns (a, b, c ... ) and I run a SELECT query on it like so:
SELECT a FROM A WHERE b IN ('....') (the ellipsis indicates a number of values to be matched to)
There's another table B which has a bunch of columns (d, e, f ... ) and I run a SELECT query on it like so:
SELECT d FROM B WHERE f = '...' (the ellipsis indicates a single value to be matched to)
Now I should say here that the two tables store different types of information about the same entity, but the columns a and d contain the exact same data (in this case, an ID). I want to find out the intersection of the two tables so I run this:
SELECT a FROM A WHERE b IN ('....') INTERSECT SELECT d FROM B WHERE f = '...'
Now here's the problem:
The first SELECT contains a set of values in the WHERE clause, right? So let's say the set is (1234, 2345,3456). Now, the result of this query when b is matched ONLY to 1234 is, let's say, abc. When it's matched to 2345, it's def, suppose. And matching to 3456, it gives abc.
Let's suppose these two results (abc and def) are also in the set of results from the second SELECT.
So, now, putting back the entire set of values to matched into the WHERE clause, the INTERSECT operation will give me abc and def. But I want abc twice since two values in the WHERE clause set match to the second SELECT.
Is there any way I can get that?
I hope it's not too complicated to understand my problem. This is a real-life problem I'm facing in my job.
Data structure and my code
Table A contains general information about a company:
company_id | branch_id | no_of_employees | city
Table B contains the financials of the company:
company_id | branch_id | revenue | profits
First SELECT:
SELECT branch_id FROM A WHERE CITY IN ('Dallas', 'Miami', 'New Orleans')
Now, running each city separately in the first SELECT, I get the branch_ids:
branch_id | city
23 | Dallas
45 | Miami
45 | New Orleans
Once again, this seems impractical as to how two cities can have the same branch ids, but please bear with me on this.
Second SELECT:
SELECT branch_id FROM B
WHERE REVENUE = 5000000
I know this is a little impractical, but for the purpose of this example, it suffices.
Running this query I get the following set:
11
23
45
22
10
So the INTERSECT will give me just 23 and 45. But I want 45 twice, since both Miami and New Orleans have that branch_id and that branch_id has generated a revenue of 5 million.
Directly from Microsoft's documentation (https://msdn.microsoft.com/en-us/library/ms188055.aspx)
:
"INTERSECT returns distinct rows that are output by both the left and right input queries operator."
So NO, it is not possible to get the same value twice when using INTERSECT because the results will be DISTINCT. However if you build an INNER JOIN correctly you can do essentially the same thing as INTERSECT except keep the repetitive results by NOT using distinct or group by.
SELECT
A.a
FROM
A
INNER JOIN B
ON A.a = B.d
AND B.F = '....'
WHERE b IN ('....')
And for your specific Example that you edited:
SELECT
branch_id
FROM
A
INNER JOIN B
ON A.branch_id = B.branch_id
AND B.REVENUE = 5000000
WHERE A.CITY IN ('Dallas', 'Miami', 'New Orleans')
You overcomplicated your task a lot:
SELECT *
FROM A
WHERE CITY IN (...)
AND EXISTS
(
SELECT 1 FROM B
WHERE B.REVENUE = 5000000
AND B.branch_id = A.branch_id
)
INTERSECT and EXCEPT are both returning row sets with DISTINCT applied.
Regular joining/filtering operations are not performed by INTERSECT or EXCEPT.

Fill one column with data from one column in another table (randomly)

Let's say I have a table Geometry and another table Customer like this:
Geometry      Customer
City        ID    Location
----         --    --------
Berlin       1    (null)
Paris        2    (null)
London
Now I'd like to fill the column Location with data from the column City. "Randomly" would be nice but it doesn't matter at all.
I've tried
update Customer set Location = (select City from Geometry where rownum < 3);
but still getting this error: single-row subquery returns more than one row
UPDATE: I'd like to fill the whole column Location with one update statement. I'm using ORACLE. The result should look like this:
Customer
ID     Location
--      -------
1      Berlin
2      London
Does someone have any better idea?
Thank you very much!
SQL Server:
UPDATE
Customer
SET
Location = (SELECT TOP 1 City FROM Geometry ORDER BY NEWID());
Since you just want it to pick one record at "random", you need to specify the correct number of rows:
update Customer set Location = (select City from Geometry where rownum = 1);
But note that since the subquery is not correlated to the Customer at all, the subquery may be optimised to only run once, and the same (randomly chosen) City will probably be used to update all Locations.
I would do the following, create a trigger on customer:
CREATE OR REPLACE TRIGGER customer_location
BEFORE INSERT OR UPDATE OF location ON customer
FOR EACH ROW
WHEN (NVL(new.location, 'X') = 'X')
BEGIN
SELECT city INTO :new.location
FROM (
SELECT city FROM geometry
ORDER BY DBMS_RANDOM.VALUE
) WHERE rownum = 1;
END;
/
UPDATE customer SET location = 'X';
This will update the customers table with a matching, "random" location. The trigger will also produce a new "random" location when a record is INSERTed into customers - as long as the location to be inserted is 'X' or NULL. (This wouldn't be wise if you actually have a location X - plug in some other value there!)
It is very possible to write a stored procedure to do the same thing, but you would have to create a cursor to loop over all the rows of customers where location is NULL.
update customer set location =
(select city from (
select distinct c.id, g.city, dbms_random.value from customer c, geometry g
order by value, city, id
) randoms where randoms.id = customer.id and rownum=1);
distinct necessary if there were two equal random values for one id

Oracle sql - identify and update primary and secondary rows in same table

I have a scenario where i need to identify a combination of records which are duplicates and use them to mark and identify which one is primary and which is secondary and then use an additional column to update the keys. This will help me update another child table which has referential integrity. Here's an example
Table Member
ID Name Birthdt MID
1 SK 09/1988 312
2 SK 09/1988 999
3 SL 06/1990 315
4 GK 08/1990 316
5 GK 08/1990 999
So from the above table my logic to identify duplicate is -- I do a group by on Name and Birthdate and when MID is 999 i consider that as a duplicate
so I created another temp table to capture dups.
Table member_dups
ID NAME BIRTHDT MID M_flg M_Key
1 SK 09/1988 P 1
2 SK 09/1988 S 1
4 GK 08/1990 P 4
5 GK 08/1990 S 4
For my table member_dups im able to load the duplicate records and update the flag . however I'm finding it difficult to get the right M_KEY for records marked as secondary. If i can achieve that then I can take that record and update the other table accordingly.
Thanks for any help offered.
If I understand your logic right the records that had MID = 999 is the secondary in member_dups.
If so you should be able to use an simple update with a join:
update member_dups
set m_key = m.id
from member_dups md
inner join Member m on m.name = mb.name and m.birthdt = mb.birthdt
where mb.m_flg = 's' and m.mid = 999
This example uses MSSQL syntax and thus isn't valid Oracle syntax, but you should get the idea, and hopefully you know Oracle better than I do. I'll try to work out the correct Oracle syntax and update the answer soon.
EDIT: I think this is the working Oracle syntax, but I haven't been able to test it:
MERGE INTO member_dups
USING
(
SELECT id,
name,
birthdt
FROM Member
where m.mid = 999
) m ON (m.name = mb.name and m.birthdt = mb.birthdt and mb.m_flg = 's')
WHEN MATCHED THEN UPDATE
SET member_dups.m_key = m.id

Oracle SQL: update table conditionally based on values in another table

[Previous essay-title for question]
Oracle SQL: update parent table column if all child table rows have specific value in a column. Update RANK of only those students who have 100 marks in all the subjects. If student has less than 100 marks in any subject, his RANK should not be updated.
I have a scenario where I have a parent table and a child table. The child table has a foreign key to parent table. I need to update parent table's status column when a column in child table rows have specific values. There are more than one child records for each parent, in some cases none. Is it possible to achieve this with Oracle SQL, without using PL/SQL. Is that possible, can some one explain how? In some case I have to update parent table row's column based on two columns of child table records.
My exact problem is like : I have two tables STUDENTS, MARKS. MARKS has a FK to STUDENTS named STUDENT_ID.MARKS has number of rows for a STUDENT record, depending on different subjects (MARKS has a FK to SUBJECTS), and has a column named MARKS_OBTAINED. I have to check that if MARKS_OBTAINED for one student for every subject (i.e. all his records in MARKS) have value 100, then update STUDENT table's column RANK to a value 'Merit'. This query:
update STUDENT
set RANK = 'Merit'
where exists ( select *
from MARKS
where MARKS.STUDENT_ID = STUDENT.ID
and MARKS.MARKS_OBTAINED = 100)
and not exists ( select *
from MARKS
where MARKS.STUDENT_ID = STUDENT.ID
and MARKS.MARKS_OBTAINED != 100)
updates all those student who have 100 marks in any subject. It does not exclude records which have non 100 marks. Because it passes rows for a STUDENT in MARKS where one record in MARKS has 100 MARKS_OBTAINED but other records have less than 100 marks, but since STUDENT obtained 100 marks in one subject, its RANK will also get updated. The requirement is that if any STUDENT records has a MARKS record with non 100 value in MARKS_OBTAINED column this STUDENT record should get excluded from the query.
Total rewrite
This is a complete rewrite to fit my example to the OQ's revised question. Unfortunately Manish has not actually run my original solution otherwise they would realise the following assertion is wrong:
Your solution returns all those
student who have 100 marks in any
subject. It does not exclude records
which have non 100 marks.
Here are six students and their marks.
SQL> select * from student
2 /
ID RANK
---------- ----------
1 normal
2 normal
3 normal
4 normal
5 normal
6 normal
6 rows selected.
SQL> select * from marks
2 /
COURSE_ID STUDENT_ID MARK
---------- ---------- ----------
1 1 100
2 1 100
1 2 100
2 2 99
1 4 100
2 5 99
1 6 56
2 6 99
8 rows selected.
SQL>
Student #1 has two courses with marks of 100. Student #4 has just the one course but with with a mark of 100. Student #2 has a mark of 100 in one course but only 99 in the other course they have taken. None of the other students scored 100 in any course. Which students will be awarded a 'merit?
SQL> update student s
2 set s.rank = 'merit'
3 where exists ( select null
4 from marks m
5 where m.student_id = s.id
6 and m.mark = 100 )
7 and not exists ( select null
8 from marks m
9 where m.student_id = s.id
10 and m.mark != 100)
11 /
2 rows updated.
SQL>
SQL> select * from student
2 /
ID RANK
---------- ----------
1 merit
2 normal
3 normal
4 merit
5 normal
6 normal
6 rows selected.
SQL>
And lo! Only those students with 100 marks in all their courses have been updated. Never underestimate the power of an AND.
So the teaching is: an ounce of testing is worth sixteen tons of supposition.
Your question is a little too vague at the moment to really answer fully. What happens to a parent row if it has no children? What happens if some of the child rows have specific values but not all of them? In the two-column case, what combinations of number of children/values are needed (is is the same set of values for each column or unique ones? Is it an AND relationship or an OR relationship)? Etc...
Anyway, making the assumption that there needs to be at least one child row with a value in a given domain, this should be fairly straightforward:
update PARENT set STATUS = 'whatever'
where ID in (
select parent_id from CHILD
where value_col in ('your', 'specific', 'values', 'here')
);
This general pattern expands to the multi-column case easily (just add an extra AND or ORed condition to the inner where clause), and to the negative case too (change where ID in to where ID not in).
If performance of this update is an issue you may want to look at triggers - at the price of slightly slower inserts on the child tables, you can keep your parent table up-to-date on an ongoing basis without having to run this update statement periodically. This works quite nicely because the logic of inspecting each child row is essentially distributed across each individual insert or update on the child table. Of course, if those child modifications are performance-critical, or if the child changes many times in between the points where you need to update the parent, then this wouldn't work very well.
What about:
UPDATE ParentTable
SET StatusColumn = 78
WHERE PK_Column IN
(SELECT DISTINCT FK_Column
FROM ChildTable AS C1
WHERE (SELECT COUNT(*) FROM ChildTable C2
WHERE C1.FK_Column = C2.FK_Column) =
(SELECT COUNT(*) FROM ChildTable C3
WHERE C1.FK_Column = C3.FK_Column
AND C3.OtherColumn = 23)
)
I strongly suspect there are neater ways to do it, but...the correlated sub-queries count the number of rows in the child table for a particular parent and the number of rows in the child table for the same parent where some filter condition matches a particular value. Those FK_Column values are returned to the main UPDATE statement, giving a list of primary key values for which the status should be updated.
This code enforces the stringent condition 'all matching rows in the child table satisfy the specific condition'. If your condition is simpler, your sub-query can be correspondingly simpler.