Update table in Postgresql by grouping rows - sql

I want to update a table by grouping (or combining) some rows together based on a certain criteria. I basically have this table currently (I want to group by 'id_number' and 'date' and sum 'count'):
Table: foo
---------------------------------------
| id_number | date | count |
---------------------------------------
| 1 | 2001 | 1 |
| 1 | 2001 | 2 |
| 1 | 2002 | 1 |
| 2 | 2001 | 6 |
| 2 | 2003 | 12 |
| 2 | 2003 | 2 |
---------------------------------------
And I want to get this:
Table: foo
---------------------------------------
| id_number | date | count |
---------------------------------------
| 1 | 2001 | 3 |
| 1 | 2002 | 1 |
| 2 | 2001 | 6 |
| 2 | 2003 | 14 |
---------------------------------------
I know that I can easily create a new table with the pertinent info. But how can I modify an existing table like this without making a "temp" table? (Note: I have nothing against using a temporary table, I'm just interested in seeing if I can do it this way)

If you want to delete rows you can add a primary key (for distinguish rows) and use two sentences, an UPDATE for the sum and a DELETE for obtain less rows.
You can do something like this:
create table foo (
id integer primary key,
id_number integer,
date integer,
count integer
);
insert into foo values
(1, 1 , 2001 , 1 ),
(2, 1 , 2001 , 2 ),
(3, 1 , 2002 , 1 ),
(4, 2 , 2001 , 6 ),
(5, 2 , 2003 , 12 ),
(6, 2 , 2003 , 2 );
select * from foo;
update foo
set count = count_sum
from (
select id, id_number, date,
sum(count) over (partition by id_number, date) as count_sum
from foo
) foo_added
where foo.id_number = foo_added.id_number
and foo.date = foo_added.date;
delete from foo
using (
select id, id_number, date,
row_number() over (partition by id_number, date order by id) as inner_order
from foo
) foo_ranked
where foo.id = foo_ranked.id
and foo_ranked.inner_order <> 1;
select * from foo;
You can try it here: http://rextester.com/PIL12447
With only one UPDATE
(but with a trigger) you can set a NULL value in count and trigger a DELETE in that case.
create table foo (
id integer primary key,
id_number integer,
date integer,
count integer
);
create function delete_if_count_is_null() returns trigger
language plpgsql as
$BODY$
begin
if new.count is null then
delete from foo
where id = new.id;
end if;
return new;
end;
$BODY$;
create trigger delete_if_count_is_null
after update on foo
for each row
execute procedure delete_if_count_is_null();
insert into foo values
(1, 1 , 2001 , 1 ),
(2, 1 , 2001 , 2 ),
(3, 1 , 2002 , 1 ),
(4, 2 , 2001 , 6 ),
(5, 2 , 2003 , 12 ),
(6, 2 , 2003 , 2 );
select * from foo;
update foo
set count = case when inner_order = 1 then count_sum else null end
from (
select id, id_number, date,
sum(count) over (partition by id_number, date) as count_sum,
row_number() over (partition by id_number, date order by id) as inner_order
from foo
) foo_added
where foo.id_number = foo_added.id_number
and foo.date = foo_added.date
and foo.id = foo_added.id;
select * from foo;
You can try it in: http://rextester.com/MWPRG10961

Related

Change Position of Serial Number in SQL

I have a table named students. the structure is given below
______________________________
AdmissionNo RollNo Name
______________________________
1001 1 A
1003 2 B
1005 3 C
1006 4 D
1008 5 E
Now i want to change rollno 4 to 2 and increment forthcoming numbers
so the result should be like below
-------------------------------
AdmissionNo RollNo Name
-------------------------------
1001 1 A
1006 2 D
1003 3 B
1005 4 C
1008 5 E
--------------------------------
How to attain this using sql Query.
Note: Question Edited as per 'The Impaler' said.Admission number is not changing.only Roll no change. The values in table are examples actual values are hundreds of records.
With the omission of a dialect, I have answered this in T-SQL, as I wanted a stab at this.
This isn't pretty, however, I use a couple of updatable CTE's to find the offset for the specific rows, and then update the needed rows accordingly:
USE Sandbox;
GO
CREATE TABLE dbo.YourTable (AdmissionNo int, Rollno tinyint, [Name] char(1));
INSERT INTO dbo.YourTable
VALUES(1001,1,'A'),
(1003,2,'B'),
(1005,3,'C'),
(1006,4,'D'),
(1008,5,'E');
GO
DECLARE #NewPosition tinyint = 2,
#MovingName char(1) = 'D';
WITH Offsetting AS(
SELECT *,
COUNT(CASE Rollno WHEN #NewPosition THEN 1 END) OVER (ORDER BY RollNo ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) -
COUNT(CASE [Name] WHEN #MovingName THEN 1 END) OVER (ORDER BY RollNo ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS LagOffset
FROM dbo.YourTable),
NewNames AS(
SELECT *,
CASE RollNo WHEN #NewPosition THEN #MovingName
ELSE LAG([Name],LagOffset) OVER (ORDER BY RollNo)
END AS NewName
FROM Offsetting)
UPDATE NewNames
SET [Name] = NewName;
GO
SELECT *
FROM dbo.YourTable;
GO
DROP TABLE dbo.YourTable;
Not pretty but you could use some sub queries
DROP TABLE IF EXISTS T;
create table t
(AdmissionNo int, RollNo int, Name varchar(1));
insert into t values
(1001 , 1 , 'A'),
(1003 , 2 , 'B'),
(1005 , 3 , 'C'),
(1006 , 4 , 'D'),
(1008 , 5 , 'E');
select t.*,
case when rollno = 2 then (select name from t where rollno = 4)
when rollno > 2 and
rollno <> (select max(rollno) from t) then (select name from t t1 where t1.rollno < t.rollno order by t1.rollno desc limit 1)
else name
end
from t;
+-------------+--------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| 1001 | 1 | A | A |
| 1003 | 2 | B | D |
| 1005 | 3 | C | B |
| 1006 | 4 | D | C |
| 1008 | 5 | E | E |
+-------------+--------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
5 rows in set (0.001 sec)
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table
(admission_no INT NOT NULL PRIMARY KEY
,roll_no INT NOT NULL
,name CHAR(1) NOT NULL
);
INSERT INTO my_table VALUES
(1001,1,'A'),
(1003,2,'B'),
(1005,3,'C'),
(1006,4,'D'),
(1008,5,'E');
SELECT *
, CASE WHEN roll_no = 4 THEN 2
WHEN roll_no >= 2 AND roll_no < 4 THEN roll_no + 1
ELSE roll_no END x FROM my_table;
+--------------+---------+------+---+
| admission_no | roll_no | name | x |
+--------------+---------+------+---+
| 1001 | 1 | A | 1 |
| 1003 | 2 | B | 3 |
| 1005 | 3 | C | 4 |
| 1006 | 4 | D | 2 |
| 1008 | 5 | E | 5 |
+--------------+---------+------+---+
5 rows in set (0.00 sec)
...or, as an update...
UPDATE my_table x
JOIN
( SELECT *
, CASE WHEN roll_no = 4 THEN 2
WHEN roll_no >= 2 AND roll_no < 4 THEN roll_no + 1
ELSE roll_no END n
FROM my_table
) y
ON y.admission_no = x.admission_no
SET x.admission_no = y.n;
You'd probably want to extend this idea to deal with the fact that rows can be dragged up and down the list, so something like this...
SET #source = 1, #target = 5;
SELECT *
, CASE WHEN roll_no = GREATEST(#source,#target) THEN LEAST(#source,#target)
WHEN roll_no >= LEAST(#source,#target) AND roll_no < GREATEST(#source,#target) THEN roll_no + 1
ELSE roll_no END x
FROM my_table;
Try this below query
; with cte as (select a.AdmissionNo, a.RollNo, b.Name from student a
join student b on a.RollNo=b.RollNo+1
where a.RollNo between 3 and 4
union all
select a.AdmissionNo, a.RollNo, b.Name from student a
left join student b on a.RollNo+2=b.RollNo
where a.RollNo=2)
update a set a.Name = b.name
from student a
join cte b on a.rollno=b.rollno

SQL How to filter table with values having more than one unique value of another column

I have data table Customers that looks like this:
ID | Sequence No |
1 | 1 |
1 | 2 |
1 | 3 |
2 | 1 |
2 | 1 |
2 | 1 |
3 | 1 |
3 | 2 |
I would like to filter the table so that only IDs with more than 1 distinct count of Sequence No remain.
Expected output:
ID | Sequence No |
1 | 1 |
1 | 2 |
1 | 3 |
3 | 1 |
3 | 2 |
I tried
select ID, Sequence No
from Customers
where count(distinct Sequence No) > 1
order by ID
but I'm getting error. How to solve this?
You can get the desired result by using the below query. This is similar to what you were trying -
Sample Table & Data
Declare #Data table
(Id int, [Sequence No] int)
Insert into #Data
values
(1 , 1 ),
(1 , 2 ),
(1 , 3 ),
(2 , 1 ),
(2 , 1 ),
(2 , 1 ),
(3 , 1 ),
(3 , 2 )
Query
Select * from #Data
where ID in(
select ID
from #Data
Group by ID
Having count(distinct [Sequence No]) > 1
)
Using analytic functions, we can try:
WITH cte AS (
SELECT *, MIN([Sequence No]) OVER (PARTITION BY ID) min_seq,
MAX([Sequence No]) OVER (PARTITION BY ID) max_seq
FROM Customers
)
SELECT ID, [Sequence No]
FROM cte
WHERE min_seq <> max_seq
ORDER BY ID, [Sequence No];
Demo
We are checking for a distinct count of sequence number by asserting that the minimum and maximum sequence numbers are not the same for a given ID. The above query could benefit from the following index:
CREATE INDEX idx ON Customers (ID, [Sequence No]);
This would let the min and max values be looked up faster.

Get records having the same value in 2 columns but a different value in a 3rd column

I am having trouble writing a query that will return all records where 2 columns have the same value but a different value in a 3rd column. I am looking for the records where the Item_Type and Location_ID are the same, but the Sub_Location_ID is different.
The table looks like this:
+---------+-----------+-------------+-----------------+
| Item_ID | Item_Type | Location_ID | Sub_Location_ID |
+---------+-----------+-------------+-----------------+
| 1 | 00001 | 20 | 78 |
| 2 | 00001 | 110 | 124 |
| 3 | 00001 | 110 | 124 |
| 4 | 00002 | 3 | 18 |
| 5 | 00002 | 3 | 25 |
+---------+-----------+-------------+-----------------+
The result I am trying to get would look like this:
+---------+-----------+-------------+-----------------+
| Item_ID | Item_Type | Location_ID | Sub_Location_ID |
+---------+-----------+-------------+-----------------+
| 4 | 00002 | 3 | 18 |
| 5 | 00002 | 3 | 25 |
+---------+-----------+-------------+-----------------+
I have been trying to use the following query:
SELECT *
FROM Table1
WHERE Item_Type IN (
SELECT Item_Type
FROM Table1
GROUP BY Item_Type
HAVING COUNT (DISTINCT Sub_Location_ID) > 1
)
But it returns all records with the same Item_Type and a different Sub_Location_ID, not all records with the same Item_Type AND Location_ID but a different Sub_Location_ID.
This should do the trick...
-- some test data...
IF OBJECT_ID('tempdb..#TestData', 'U') IS NOT NULL
BEGIN DROP TABLE #TestData; END;
CREATE TABLE #TestData (
Item_ID INT NOT NULL PRIMARY KEY,
Item_Type CHAR(5) NOT NULL,
Location_ID INT NOT NULL,
Sub_Location_ID INT NOT NULL
);
INSERT #TestData (Item_ID, Item_Type, Location_ID, Sub_Location_ID) VALUES
(1, '00001', 20, 78),
(2, '00001', 110, 124),
(3, '00001', 110, 124),
(4, '00002', 3, 18),
(5, '00002', 3, 25);
-- adding a covering index will eliminate the sort operation...
CREATE NONCLUSTERED INDEX ix_indexname ON #TestData (Item_Type, Location_ID, Sub_Location_ID, Item_ID);
-- the actual solution...
WITH
cte_count_group AS (
SELECT
td.Item_ID,
td.Item_Type,
td.Location_ID,
td.Sub_Location_ID,
cnt_grp_2 = COUNT(1) OVER (PARTITION BY td.Item_Type, td.Location_ID),
cnt_grp_3 = COUNT(1) OVER (PARTITION BY td.Item_Type, td.Location_ID, td.Sub_Location_ID)
FROM
#TestData td
)
SELECT
cg.Item_ID,
cg.Item_Type,
cg.Location_ID,
cg.Sub_Location_ID
FROM
cte_count_group cg
WHERE
cg.cnt_grp_2 > 1
AND cg.cnt_grp_3 < cg.cnt_grp_2;
You can use exists :
select t.*
from table t
where exists (select 1
from table t1
where t.Item_Type = t1.Item_Type and
t.Location_ID = t1.Location_ID and
t.Sub_Location_ID <> t1.Sub_Location_ID
);
Sql server has no vector IN so you can emulate it with a little trick. Assuming '#' is illegal char for Item_Type
SELECT *
FROM Table1
WHERE Item_Type+'#'+Cast(Location_ID as varchar(20)) IN (
SELECT Item_Type+'#'+Cast(Location_ID as varchar(20))
FROM Table1
GROUP BY Item_Type, Location_ID
HAVING COUNT (DISTINCT Sub_Location_ID) > 1
);
The downsize is the expression in WHERE is non-sargable
I think you can use exists:
select t1.*
from table1 t1
where exists (select 1
from table1 tt1
where tt1.Item_Type = t1.Item_Type and
tt1.Location_ID = t1.Location_ID and
tt1.Sub_Location_ID <> t1.Sub_Location_ID
);

Insert many rows returning id and update that ids in another table

I am working with PostgreSQL, My sql structure:
CREATE TEMP TABLE users (
id_user serial,
user_name varchar,
id_user_description int
);
CREATE TEMP TABLE user_description (
id_user_description serial,
age int
);
users table has some users:
INSERT INTO users (user_name)
SELECT column1
FROM (
VALUES
('John'),
('Amanda')
) t;
I am trying to insert data to table user_description and I also need to update inserted row ids to table users. My query is this:
WITH inserted_user_description AS (
INSERT INTO user_description (age)
SELECT age
FROM (
SELECT users.id_user,
t.column1 AS age,
t.column2 AS user_name
FROM (
VALUES
(21, 'John'),
(28, 'Amanda')
) t
INNER JOIN users ON users.user_name = t.column2
) tt
RETURNING id_user_description, tt.id_user
)
UPDATE users SET id_user_description = t.id_user_description
FROM (
SELECT id_user_description, id_user
FROM inserted_user_description
) t
WHERE users.id_user = t.id_user;
But I get error:
ERROR: missing FROM-clause entry for table "tt"
LINE 15: RETURNING id_user_description, tt.id_user
How could I fix this?
Here is a valid SQL snippet that illustrated how it works. You have 2 tables a and b. You want to update b when you insert rows in a.
a and b schema:
CREATE TABLE a (
id serial unique,
some_int int
);
CREATE TABLE b (
id serial,
a_id int,
some_date timestamp
);
Let's insert some rows into b to match the ones we will insert in a (they are the rows we will update):
INSERT INTO b (a_id, some_date)
SELECT generate_series, null
FROM generate_series(1, 100);
Now, here is how to insert rows in a and update equivalent rows in b:
WITH inserted as (
INSERT INTO a (some_int)
SELECT *
FROM generate_series(1, 10)
RETURNING id
)
UPDATE b
SET some_date = NOW()
FROM inserted i
WHERE i.id = b.a_id
;
As you can see, 10 rows where inserted in a and the 10 equivalent rows where updated in b:
test=# SELECT * FROM a;
id | some_int
----+----------
1 | 1
2 | 2
3 | 3
4 | 4
5 | 5
6 | 6
7 | 7
8 | 8
9 | 9
10 | 10
(10 rows)
test=# SELECT * FROM b WHERE some_date IS NOT NULL;
id | a_id | some_date
----+------+----------------------------
1 | 1 | 2017-03-16 17:48:32.257217
2 | 2 | 2017-03-16 17:48:32.257217
3 | 3 | 2017-03-16 17:48:32.257217
4 | 4 | 2017-03-16 17:48:32.257217
5 | 5 | 2017-03-16 17:48:32.257217
6 | 6 | 2017-03-16 17:48:32.257217
7 | 7 | 2017-03-16 17:48:32.257217
8 | 8 | 2017-03-16 17:48:32.257217
9 | 9 | 2017-03-16 17:48:32.257217
10 | 10 | 2017-03-16 17:48:32.257217
(10 rows)
Update:
In your specific case, this is what I believe your query should look like (always hard to write queries without the schema!):
WITH inserted_user_description AS (
INSERT INTO user_description (age, <...>)
SELECT u.id_user,
t.column1 AS age,
<...>
t.column8 AS user_name
FROM (
VALUES
(21, <...> ,'John'),
(28, <...> ,'Amanda'),
<...>
) t
JOIN users u ON u.user_name = t.user_name
RETURNING id_user_description, u.id_user
)
UPDATE users
SET id_user_description = t.id_user_description
FROM inserted_user_description t
WHERE users.id_user = t.id_user;

How to copy rows into a new a one to many relationship

I'm trying to copy a set of data in a one to many relationship to create a new set of the same data in a new, but unrelated one to many relationship. Lets call them groups and items. Groups have a 1-* relation with items - one group has many items.
I've tried to create a CTE to do this, however I can't get the items inserted (in y) as the newly inserted groups don't have any items associated with them yet. I think I need to be able to access old. and new. like you would in a trigger, but I can't work out how to do this.
I think I could solve this by introducing a previous parent id into the templateitem table, or maybe a temp table with the data required to enable me to join on that, but I was wondering if it is possible to solve it this way?
SQL Fiddle Keeps Breaking on me, so I've put the code here as well:
DROP TABLE IF EXISTS meta.templateitem;
DROP TABLE IF EXISTS meta.templategroup;
CREATE TABLE meta.templategroup (
templategroup_id serial PRIMARY KEY,
groupname text,
roworder int
);
CREATE TABLE meta.templateitem (
templateitem_id serial PRIMARY KEY,
itemname text,
templategroup_id INTEGER NOT NULL REFERENCES meta.templategroup(templategroup_id)
);
INSERT INTO meta.templategroup (groupname, roworder) values ('Group1', 1), ('Group2', 2);
INSERT INTO meta.templateitem (itemname, templategroup_id) values ('Item1A',1), ('Item1B',1), ('Item2A',2);
WITH
x AS (
INSERT INTO meta.templategroup (groupname, roworder)
SELECT distinct groupname || '_v1' FROM meta.templategroup where templategroup_id in (1,2)
RETURNING groupname, templategroup_id, roworder
),
y AS (
Insert INTO meta.templateitem (itemname, templategroup_id)
Select itemname, x.templategroup_id
From meta.templateitem i
INNER JOIN x on x.templategroup_id = i.templategroup_id
RETURNING *
)
SELECT * FROM y;
Use an auxiliary column templategroup.old_id:
ALTER TABLE meta.templategroup ADD old_id int;
WITH x AS (
INSERT INTO meta.templategroup (groupname, roworder, old_id)
SELECT DISTINCT groupname || '_v1', roworder, templategroup_id
FROM meta.templategroup
WHERE templategroup_id IN (1,2)
RETURNING templategroup_id, old_id
),
y AS (
INSERT INTO meta.templateitem (itemname, templategroup_id)
SELECT itemname, x.templategroup_id
FROM meta.templateitem i
INNER JOIN x ON x.old_id = i.templategroup_id
RETURNING *
)
SELECT * FROM y;
templateitem_id | itemname | templategroup_id
-----------------+----------+------------------
4 | Item1A | 3
5 | Item1B | 3
6 | Item2A | 4
(3 rows)
It's impossible to do that in a single plain sql query without an additional column. You have to store the old ids somewhere. As an alternative you can use plpgsql and anonymous code block:
Before:
select *
from meta.templategroup
join meta.templateitem using (templategroup_id);
templategroup_id | groupname | roworder | templateitem_id | itemname
------------------+-----------+----------+-----------------+----------
1 | Group1 | 1 | 1 | Item1A
1 | Group1 | 1 | 2 | Item1B
2 | Group2 | 2 | 3 | Item2A
(3 rows)
Insert:
do $$
declare
grp record;
begin
for grp in
select distinct groupname || '_v1' groupname, roworder, templategroup_id
from meta.templategroup
where templategroup_id in (1,2)
loop
with insert_group as (
insert into meta.templategroup (groupname, roworder)
values (grp.groupname, grp.roworder)
returning templategroup_id
)
insert into meta.templateitem (itemname, templategroup_id)
select itemname || '_v1', g.templategroup_id
from meta.templateitem i
join insert_group g on grp.templategroup_id = i.templategroup_id;
end loop;
end $$;
After:
select *
from meta.templategroup
join meta.templateitem using (templategroup_id);
templategroup_id | groupname | roworder | templateitem_id | itemname
------------------+-----------+----------+-----------------+-----------
1 | Group1 | 1 | 1 | Item1A
1 | Group1 | 1 | 2 | Item1B
2 | Group2 | 2 | 3 | Item2A
3 | Group1_v1 | 1 | 4 | Item1A_v1
3 | Group1_v1 | 1 | 5 | Item1B_v1
4 | Group2_v1 | 2 | 6 | Item2A_v1
(6 rows)