Variable number of columns in pivot - sql

I have two tables.
First table is a dictionary:
test_1=> select * from first;
id | name
----+-------
1 | name1
2 | name2
3 | name3
Second table which is referenced to first table:
test_1=> select * from second;
id | number | first_id
----+--------+----------
1 | 1 | 2
2 | 1 | 3
3 | 1 | 2
4 | 2 | 3
5 | 2 | 2
6 | 3 | 3
Now I use pivot to show results:
SELECT *
FROM crosstab('SELECT
number, name, sum(number)::integer
FROM first
LEFT JOIN second ON second.first_id = first.id
GROUP BY 2, 1
ORDER BY 1, 2', $$SELECT unnest('{name1,name2,name3}'::text[])$$) AS ta (
number INTEGER,
name1 INTEGER,
name2 INTEGER,
name3 INTEGER
);
And result:
number | name1 | name2 | name3
--------+-------+-------+-------
1 | | 2 | 1
2 | | 2 | 2
3 | | | 3
And it's ok.
But now I must manually define columns - name1, name2, name3..., but columns are form dictionary table which can be changed.
So my question is - there is any possibility to get columns to pivot dynamically?

Related

SQL UPDATE statement based on number of rows returned from SELECT subquery

I have a table with cols ID, and account type(int)
and I want to update account type to 3 for all rows, except one where given an ID there is more than one row with the same account type. so if I have
ID --- account_type
1 --- 2
1 --- 2
2 --- 1
2 --- 2
2 --- 3
I want to change one of the rows with id = 1 to have account type = 3 but leave the other one at 2 so I would like to return for the above example.
ID --- account_type
1 --- 2
1 --- 3
2 --- 1
2 --- 2
2 --- 3
I tried
UPDATE myTable
SET account_type = 3
WHERE 1 < (
SELECT COUNT(*)
FROM myTable
GROUP BY ID, account_type
HAVING COUNT(*) > 1);
but this updated every row in my db instead of just the one row with the duplicate account type so I know I did something wrong there. and this statement would set both rows with id=1 to have account_type =3 instead of just one row. How would I accomplish this?
EDIT:
I think I can fix the problem of only updating one row with:
UPDATE myTable p1
SET account_type = 3
WHERE 1 < (
SELECT COUNT(*)
FROM myTable p2
WHERE p1.primaryKey > p2.primaryKey
GROUP BY ID, account_type
HAVING COUNT(*) > 1);
but I'm still not sure why this updating every row in the db instead of the one with the duplicate account_type
So, we have table mytable:
| PrimaryKey | ID | account_type |
| ----------- | --- | ------------- |
| 1 | 1 | 2 |
| 2 | 1 | 2 |
| 3 | 2 | 1 |
| 4 | 2 | 2 |
| 5 | 2 | 3 |
and I think, this is Your update:
update mytable
set account_type = 3
where primarykey in (
select min(primarykey)
from mytable
group by id
)
and id not in (
select id
from mytable
where account_type = 3
);
this is the result:
| PrimaryKey | ID | account_type |
| ----------- | --- | ------------- |
| 1 | 1 | 3 |
| 2 | 1 | 2 |
| 3 | 2 | 1 |
| 4 | 2 | 2 |
| 5 | 2 | 3 |

How to only show Unique values in this existing SQL query

I am comparing two tables (dbo.new and dbo.old) and if the first three column match and the forth column doesnst match, it has to select it. Now this shows a lot of values, and I only want to display the unique values of column2. This is the code I have now:
SELECT dbo.new.[column1], dbo.new.[column2], dbo.new.[column3], dbo.new.[column4]
FROM dbo.new
JOIN dbo.old ON dbo.new.[column1]=dbo.old.[column1]
AND dbo.new.[column2]=dbo.old.[column2]
AND dbo.new.[column3]=dbo.old.[column3]
WHERE [dbo].[new].[column4] <> [dbo].[old].[column4]
First two tables I start with:
-----------------
| 1 | 1 | 1 | 1 |
-----------------
| 2 | 1 | 2 | 2 |
-----------------
| 3 | 3 | 3 | 3 |
-----------------
| 4 | 1 | 4 | 4 |
-----------------
-----------------
| 1 | 1 | 1 | 9 |
-----------------
| 2 | 1 | 2 | 9 |
-----------------
| 3 | 3 | 3 | 9 |
-----------------
| 4 | 1 | 4 | 9 |
-----------------
This is the outcome of the query above:
-----------------
| 1 | 1 | 1 | 1 |
-----------------
| 2 | 1 | 2 | 2 |
-----------------
| 3 | 3 | 3 | 3 |
-----------------
| 4 | 1 | 4 | 4 |
-----------------
^ delete those duplicates
This is what I want to be the outcome:
-----------------
| 1 | 1 | 1 | 1 |
-----------------
| 3 | 3 | 3 | 3 |
-----------------
I tried many things like UNIQUE and DISTINCT but I cant find the solution. It doenst even need to show the first value, as long as it show onea row with the unique number. So this is correct too:
-----------------
| 4 | 1 | 4 | 4 |
-----------------
| 3 | 3 | 3 | 3 |
-----------------
Choose the ordering you need in over() to get proper rows.
SELECT TOP(1) WITH TIES
dbo.new.[column1], dbo.new.[column2], dbo.new.[column3], dbo.new.[column4]
FROM dbo.new
JOIN dbo.old ON dbo.new.[column1]=dbo.old.[column1]
AND dbo.new.[column2]=dbo.old.[column2]
AND dbo.new.[column3]=dbo.old.[column3]
where [dbo].[new].[column4] <> [dbo].[old].[column4]
ORDER BY row_number() over(partition by dbo.new.[column2] order by dbo.new.[column1])
Quick demo, runs OK sql server 2014
create table dbo.new(
column1 int,
column2 int,
column3 int,
column4 int);
create table dbo.old(
column1 int,
column2 int,
column3 int,
column4 int);
insert dbo.new values
( 1 , 1 , 1 , 1 ),
( 2 , 1 , 2 , 2 ),
( 3 , 3 , 3 , 3 ),
( 4 , 1 , 4 , 4 );
insert dbo.old values
( 1 , 1 , 1 , 9 ),
( 2 , 1 , 2 , 9 ),
( 3 , 3 , 3 , 9 ),
( 4 , 1 , 4 , 9 );
SELECT TOP(1) WITH TIES
dbo.new.[column1], dbo.new.[column2], dbo.new.[column3], dbo.new.[column4]
FROM dbo.new
JOIN dbo.old ON dbo.new.[column1]=dbo.old.[column1]
AND dbo.new.[column2]=dbo.old.[column2]
AND dbo.new.[column3]=dbo.old.[column3]
where [dbo].[new].[column4] <> [dbo].[old].[column4]
ORDER BY row_number() over(partition by dbo.new.[column2] order by dbo.new.[column1]);
Result is
column1 column2 column3 column4
1 1 1 1
3 3 3 3
It looks like you're only interested in column 2 so let's start with only selecting that.
Then add a simple: GROUP BY at the end and you're done.
SELECT N.[column2] as myvalue
FROM dbo.new N
JOIN dbo.old O
ON N.[column1]=O.[column1]
AND N.[column2]=O.[column2]
AND N.[column3]=O.[column3]
WHERE N.[column4] <> O.[column4]
GROUP BY N.[column2]

Select from cross-reference based on inclusion (column values being superset)

Given a cross-reference table t relating table a with b:
| id | a_id | b_id |
--------------------
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 1 | 3 |
| 4 | 2 | 7 |
| 5 | 2 | 3 |
| 6 | 3 | 2 |
| 7 | 3 | 3 |
What would be the conventional way of selecting all a_id whose b_id is a superset of a given set?
For example, for the set (2,3), I would expect the result:
| a_id |
--------
| 1 |
| 3 |
Since a_id 1 and 3 are the only set of b_id that is a superset of (2,3).
The best solution I've found so far (thanks to this answer):
select id
from a
where 2 = (select count(*)
from t
where t.a_id = a.id and t.b_id in (2,3)
);
But I'd prefer to avoid calculating stuff like cardinality before running the query.
You can simply adapt the query as:
select id
from a cross join
(select count(*) as cnt
from t
where . . .
) x
where x.cnt = (select count(*)
from t
where t.a_id = a.id and t.b_id in (2,3)
);

Select unique results and null

I need get all lines from table, that have unique value in certain fields and all lines, than have null in this fields. Example:
id | name | group
-----------------
1 | One | 1
2 | Two | null
3 | Three| 3
4 | Four | 2
5 | Five | 1
6 | Six | 2
7 | Seven| null
Result:
id | name | group
-----------------
1 | One | 1
2 | Two | null
3 | Three| 3
4 | Four | 2
7 | Seven| null
How to make it in one request?
select t.id, t.name, t.`group`
from tablename t
join (select `group`, min(id) as mid
from tablename
where `group` is not null
group by `group`) x on x.mid = t.id and x.`group` = t.`group`
union all
select id, name, `group`
from tablename
where `group` is null

Pl/PgSQL, insert into two tables, one with header rows

What would be the best way of going about doing the following insert. I've looked around, and I'm kind of stuck.
The table I Currently have (insert)
| id | order_id | item_id | type | group |
| 1 | 1 | 1 | 2 | 1 | <- type 2 represents a "header" item, or a "kit"
| 2 | 1 | 2 | 1 | 1 | <- type 1 represents a member of the "kit"
| 3 | 1 | 3 | 1 | 1 |
| 4 | 1 | 4 | 2 | 2 | <- New group means new kit
| 5 | 1 | 2 | 1 | 2 |
| 6 | 1 | 5 | 1 | 2 |
I need to insert these items into the following two tables:
1) item_entry
| id | mode | tmplt_id | item_id | parent_item_entry_id |
| 1 | 1 | 1 | NULL | NULL | <- This is a header line, mode 1
| 2 | 2 | NULL | 2 | 1 | <- This is a sub line, mode 2
| 3 | 2 | NULL | 3 | 1 | <- parent_item_entry_id references the header it belongs to
| 4 | 1 | 4 | NULL | NULL |
| 5 | 2 | NULL | 2 | 4 |
| 6 | 2 | NULL | 5 | 4 |
2) item_entry_details
| id | item_entry_id | order_id | group |
| 1 | 1 | 1 | 1 |
| 2 | 4 | 1 | 2 | <- only header information is necessary
Of course, item_entry.id is driven off of a sequence (item_entry_id_seq). Is there an elegent way of getting this to work? I currently loop through each group, first assigning a nextval() to a variable, and then I loop through each item in the group, writing to the table.
FOR recGroup IN SELECT DISTINCT group FROM insert LOOP
intParentItemEntryID := nextval('item_entry_id_seq');
FOR recLine IN SELECT * FROM insert LOOP
INSERT INTO item_entry VALUES (CASE intParentItemEntryID/DEFAULT, CASE 1/2, CASE recLine.item_id/NULL, CASE NULL/recLine.item_id, CASE NULL/intParentItemEntryID)
INSERT INTO item_entry_details VALUES (DEFAULT, intParentItemEntryID, recLine.order_id, recLine.group);
END LOOP;
END LOOP;
Is there a better way, or is the above the only way this type of insert can be done?
No need for procedural code "row-at-a-time" here, just plain old sql will suffice.
-- create the tables that the OP did not provide
DROP TABLE the_input;
CREATE TABLE the_input
( id INTEGER NOT NULL PRIMARY KEY
, order_id INTEGER NOT NULL
, item_id INTEGER NOT NULL
, ztype INTEGER NOT NULL
, zgroup INTEGER NOT NULL
);
DROP TABLE target1 ;
CREATE TABLE target1
( id INTEGER NOT NULL
, zmode INTEGER NOT NULL
, tmplt_id INTEGER
, item_id INTEGER
, parent_item_entry_id INTEGER
);
DROP TABLE target2 ;
CREATE TABLE target2
( id SERIAL NOT NULL
, item_entry_id INTEGER NOT NULL
, order_id INTEGER NOT NULL
, zgroup INTEGER NOT NULL
);
-- fil it up ...
INSERT INTO the_input
( id, order_id, item_id, ztype, zgroup ) VALUES
( 1 , 1 , 1 , 2 , 1 ) -- <- type 2 represents a "header" item, or a "kit"
,( 2 , 1 , 2 , 1 , 1 ) -- <- type 1 represents a member of the "kit"
,( 3 , 1 , 3 , 1 , 1 ) --
,( 4 , 1 , 4 , 2 , 2 ) -- <- New group means new kit
,( 5 , 1 , 2 , 1 , 2 ) --
,( 6 , 1 , 5 , 1 , 2 ) --
;
-- Do the inserts.
INSERT INTO target1(id,zmode,tmplt_id,item_id,parent_item_entry_id)
SELECT i1.id, 1, i1.id, NULL, NULL
FROM the_input i1
WHERE i1.ztype=2
UNION ALL
SELECT i2.id, 2, NULL, i2.id, ip.item_id
FROM the_input i2
JOIN the_input ip ON ip.zgroup = i2.zgroup AND ip.ztype=2
WHERE i2.ztype=1
;
INSERT INTO target2(item_entry_id,order_id,zgroup)
SELECT DISTINCT MIN(item_id),order_id, zgroup
FROM the_input i1
WHERE i1.ztype=2
GROUP BY order_id,zgroup
;
SELECT * FROM target1
ORDER BY id;
SELECT * FROM target2
ORDER BY id;
Result:
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "the_input_pkey" for table "the_input"
CREATE TABLE
INSERT 0 6
DROP TABLE
CREATE TABLE
INSERT 0 6
id | zmode | tmplt_id | item_id | parent_item_entry_id
----+-------+----------+---------+----------------------
1 | 1 | 1 | |
2 | 2 | | 2 | 1
3 | 2 | | 3 | 1
4 | 1 | 4 | |
5 | 2 | | 5 | 4
6 | 2 | | 6 | 4
(6 rows)
DROP TABLE
NOTICE: CREATE TABLE will create implicit sequence "target2_id_seq" for serial column "target2.id"
CREATE TABLE
INSERT 0 2
id | item_entry_id | order_id | zgroup
----+---------------+----------+--------
1 | 4 | 1 | 2
2 | 1 | 1 | 1
(2 rows)