Postgresql/SQL insert into table based on multiple column values - sql

My origin table looks like this
company_name | feature_name | feature_value
abc | income | 315
abc | cash | 213
abc | val | 9
goo | income | 123
goo | cash | 487
goo | val | 990
I want to insert into a new table so that the new table looks like this, and the new table won't contain column for cash.
company_name | income_name | income_value | val_name | val_value
abc | income | 315 | val | 9
goo | income | 123 | val | 990
I've checked a lot of posts but still don't know how to do that. Any suggestion is appreciated.

To pivot data without using any proprietary syntax you have a couple of options. Group/max on conditionals or left join multiple times:
SELECT
company_name,
MAX(CASE WHEN feature_name = 'income' THEN feature_value END as income,
MAX(CASE WHEN feature_name = 'val' THEN feature_value END as val,
...
FROM
table
GROUP BY company name
To see how it works run it without the group by and max operations; you'll see how the case when spreads the single value column out into multiple columns, multiple rows per company. The group by and max collapse these multiple rows to a single row by removing all the nulls
If your table is just a snippet you can add more columns where the ... is by copying the pattern. Remember to remove the trailing comma from the select list items
The join way:
SELECT
t.company_name,
income.feature_value as income,
val.feature_value as val,
...
FROM
table t
LEFT JOIN table income ON t.company_name = income.company_name and income.feature_name = 'income'
LEFT JOIN table val ON t.company_name = val.company_name and val.feature_name = 'val'
...
This virtually splits the table into multiple tables each having the company name and just some of the rows, making a table per feature, then joins them all back together into one multi column result set
I've always preferred the group way but you'd have to trial which was more efficient for your situation and which is easier to maintain for your understanding
Inserting data from a select into another table is a very common operation so I've omitted that part for clarity- this is just to show you how to pivot the data. The insert will be one of:
INSERT INTO existingtable(company_name, income, val)
SELECT ...
CREATE newtable AS
SELECT ...

You can try with conditional aggregation using case when expression
select company_name,
max(case when feature_name='income' then feature_name end) as income_name,
max(case when feature_name='income' then feature_name end) as income_value,
max(case when feature_name='val' then feature_name end) as val_name,
max(case when feature_name='val' then feature_name end) as val_value
from tablename
group by company_name

Related

How to create a table to count with a conditional

I have a database with a lot of columns with pass, fail, blank indicators
I want to create a function to count each type of value and create a table from the counts. The structure I am thinking is something like
| Value | x | y | z |
|-------|------------------|-------------------|---|---|---|---|---|---|---|
| pass | count if x=pass | count if y=pass | count if z=pass | | | | | | |
| fail | count if x=fail | count if y=fail |count if z=fail | | | | | | |
| blank | count if x=blank | count if y=blank | count if z=blank | | | | | | |
| total | count(x) | count(y) | count (z) | | | | | | |
where x,y,z are columns from another table.
I don't know which could be the best approach for this
thank you all in advance
I tried this structure but it shows syntax error
CREATE FUNCTION Countif (columnx nvarchar(20),value_compare nvarchar(10))
RETURNS Count_column_x AS
BEGIN
IF columnx=value_compare
count(columnx)
END
RETURN
END
Also, I don't know how to add each count to the actual table I am trying to create
Conditional counting (or any conditional aggregation) can often be done inline by placing a CASE expression inside the aggregate function that conditionally returns the value to be aggregated or a NULL to skip.
An example would be COUNT(CASE WHEN SelectMe = 1 THEN 1 END). Here the aggregated value is 1 (which could be any non-null value for COUNT(). (For other aggregate functions, a more meaningful value would be provided.) The implicit ELSE returns a NULL which is not counted.
For you problem, I believe the first thing to do is to UNPIVOT your data, placing the column name and values side-by-side. You can then group by value and use conditional aggregation as described above to calculate your results. After a few more details to add (1) a totals row using WITH ROLLUP, (2) a CASE statement to adjust the labels for the blank and total rows, and (3) some ORDER BY tricks to get the results right and we are done.
The results may be something like:
SELECT
CASE
WHEN GROUPING(U.Value) = 1 THEN 'Total'
WHEN U.Value = '' THEN 'Blank'
ELSE U.Value
END AS Value,
COUNT(CASE WHEN U.Col = 'x' THEN 1 END) AS x,
COUNT(CASE WHEN U.Col = 'y' THEN 1 END) AS y
FROM #Data D
UNPIVOT (
Value
FOR Col IN (x, y)
) AS U
GROUP BY U.Value WITH ROLLUP
ORDER BY
GROUPING(U.Value),
CASE U.Value WHEN 'Pass' THEN 1 WHEN 'Fail' THEN 2 WHEN '' THEN 3 ELSE 4 END,
U.VALUE
Sample data:
x
y
Pass
Pass
Pass
Fail
Pass
Fail
Sample results:
Value
x
y
Pass
3
1
Fail
1
1
Blank
0
2
Total
4
4
See this db<>fiddle for a working example.
I think you don't need a generic solution like a function with value as parameter.
Perhaps, you could create a view grouping your data and after call this view filtering by your value.
Your view body would be something like that
select value, count(*) as Total
from table_name
group by value
Feel free to explain your situation better so I could help you.
You can do this by grouping by the status column.
select status, count(*) as total
from some_table
group by status
Rather than making a whole new table, consider using a view. This is a query that looks like a table.
create view status_counts as
select status, count(*) as total
from some_table
group by status
You can then select total from status_counts where status = 'pass' or the like and it will run the query.
You can also create a "materialized view". This is like a view, but the results are written to a real table. SQL Server is special in that it will keep this table up to date for you.
create materialized view status_counts with distribution(hash(status))
select status, count(*) as total
from some_table
group by status
You'd do this for performance reasons on a large table which does not update very often.

HQL, insert two rows if a condition is met

I have the following table called table_persons in Hive:
+--------+------+------------+
| people | type | date |
+--------+------+------------+
| lisa | bot | 19-04-2022 |
| wayne | per | 19-04-2022 |
+--------+------+------------+
If type is "bot", I have to add two rows in the table d1_info else if type is "per" i only have to add one row so the result is the following:
+---------+------+------------+
| db_type | info | date |
+---------+------+------------+
| x_bot | x | 19-04-2022 |
| x_bnt | x | 19-04-2022 |
| x_per | b | 19-04-2022 |
+---------+------+------------+
How can I add two rows if this condition is met?
with a Case When maybe?
You may try using a union to merge or duplicate the rows with bot. The following eg unions the first query which selects all records and the second query selects only those with bot.
Edit
In response to the edited question, I have added an additional parity column (storing 1 or 0) named original to differentiate the duplicate entry named
SELECT
p1.*,
1 as original
FROM
table_persons p1
UNION ALL
SELECT
p1.*,
0 as original
FROM
table_persons p1
WHERE p1.type='bot'
You may then insert this into your other table d1_info using the above query as a subquery or CTE with the desired transformations CASE expressions eg
INSERT INTO d1_info
(`db_type`, `info`, `date`)
WITH merged_data AS (
SELECT
p1.*,
1 as original
FROM
table_persons p1
UNION ALL
SELECT
p1.*,
0 as original
FROM
table_persons p1
WHERE p1.type='bot'
)
SELECT
CONCAT('x_',CASE
WHEN m1.type='per' THEN m1.type
WHEN m1.original=1 AND m1.type='bot' THEN m1.type
ELSE 'bnt'
END) as db_type,
CASE
WHEN m1.type='per' THEN 'b'
ELSE 'x'
END as info,
m1.date
FROM
merged_data m1
ORDER BY m1.people,m1.date;
See working demo db fiddle here
I think what you want is to create a new table that captures your logic. This would simplify your query and make it so you could easily add new types without having to edit logic of a case statement. It may also make it cleaner to view your logic later.
CREATE TABLE table_persons (
`people` VARCHAR(5),
`type` VARCHAR(3),
`date` VARCHAR(10)
);
INSERT INTO table_persons
VALUES
('lisa', 'bot', '19-04-2022'),
('wayne', 'per', '19-04-2022');
CREATE TABLE info (
`type` VARCHAR(5),
`db_type` VARCHAR(5),
`info` VARCHAR(1)
);
insert into info
values
('bot', 'x_bot', 'x'),
('bot', 'x_bnt', 'x'),
('per','x_per','b');
and then you can easily do a join:
select
info.db_type,
info.info,
persons.date date
from
table_persons persons inner join info
on
info.type = persons.type

Update or insert data from a table to another while the column name retrieved from other table

What I want to implement is following: table target with columns my_index (primary key), updated_time, my_col_1, my_col_2, the data should look like following, and whenever a value comes into table source, the corresponding row in table target should be updated, and the updated_time as well. the columns would be just a sub set of the table def. So I think the table target needs to be created once the columns received from caller.
Table target:
my_index | updated_time | ABC | DEF | GHI
---------+-------------------------+-----+-----+------
12 | 2021-05-11 01:01:01.000 | 1.0 | 2.0 | 3.0
Table source with columns id (auto incr primary key), my_index, my_id, my_value, the data should look like this:
id | my_index | my_id | my_value
---+----------+-------+---------
1 | 12 | 3 | 1.0
2 | 12 | 4 | 2.0
3 | 12 | 5 | 3.0
4 | 11 | 6 | 4.0
while the columns in table target come from another table def with columns my_id (primary key), my_col, the data should look like this:
my_id | my_col
------+--------
3 | ABC
4 | DEF
5 | GHI
6 | JKL
I am not sure how to implement a SQL command to get data from table source, then update or insert into table target.
Could somebody kindly help? Appreciate it.
This is actually not a very good data model - storing the names of columns in one table for updates in another is tricky. SQL does not deal well with variable column names.
You can do what you want, but you have to list the columns explicitly. The idea is to summarize the source table to get values for each potential column. Then, set the value in the target column when there is a value in the source column:
update t
set abc = (case when s.abc is not null then s.abc else t.abc end),
def = (case when s.def is not null then s.def else t.def end),
ghi = (case when s.ghi is not null then s.ghi else t.ghi end)
from target t join
(select s.my_index,
max(case when d.my_col = 'ABC' then s.my_value end) as abc,
max(case when d.my_col = 'DEF' then s.my_value end) as def,
max(case when d.my_col = 'GHI' then s.my_value end) as ghi
from source s join
def d
on s.my_id = d.my_id
group by s.my_index
) s
on s.my_index = t.my_index

How can I summarise this JSON array data as columns on the same row?

I have a PostgreSQL database with a table called items which includes a JSONB field care_ages.
This field contains an array of between one and three objects, which include these keys:
AgeFrom
AgeTo
Number
Register
For a one-off audit report I need to run on this table, I need to "unpack" this field into columns on the same row.
I've used jsonb_to_recordset to split it out into rows and columns, which gets me halfway:
SELECT
items.id,
items.name,
care_ages.*
FROM
ofsted_items,
jsonb_to_recordset(items.care_age) AS care_ages ("AgeFrom" integer, "AgeTo" integer, "Register" text, "MaximumNumber" integer)
This gives me output like:
| id | name | register | age_from | age_to | maximum_number |
|----|--------------|----------|----------|--------|----------------|
| 1 | namey mcname | xyz | 0 | 4 | 5 |
| 1 | namey mcname | abc | 4 | 8 | 7 |
Next, I need to combine these rows together, perhaps using GROUP BY, adding extra columns, like this:
| id | name | register_xyz? | xyz_age_from | xyz_age_to | xyz_maximum_number | register_abc? | abc_age_from | abc_age_to | abc_maximum_number |
|----|--------------|---------------|--------------|------------|--------------------|---------------|--------------|------------|--------------------|
| 1 | namey mcname | true | 0 | 4 | 5 | true | 4 | 8 | 7 |
Because I know ahead of time which "registers" there are (there's only three of them), it seems like this should be possible.
I've tried following this example, using CASE to calculate extra columns, but I'm not getting any useful values: all 0s and 5s for some reason.
If you are using Postgres 12 or later, you can use a jsonpath query to first extract the JSON object for each register into separate columns. Then use the "usualy" operators to extract the keys. This avoids first expanding into multiple rows just to aggregate them back into a single row later
select id, name,
(reg_xyz ->> 'AgeFrom')::int as xyz_age_from,
(reg_xyz ->> 'AgeTo')::int as xyz_age_to,
(reg_xyz ->> 'Number')::int as xyz_max_num,
(reg_abc ->> 'AgeFrom')::int as abc_age_from,
(reg_abc ->> 'AgeTo')::int as abc_age_to,
(reg_abc ->> 'Number')::int as abc_max_num
from (
select id, name,
jsonb_path_query_first(care_age, '$[*] ? (#.Register == "xyz")') as reg_xyz,
jsonb_path_query_first(care_age, '$[*] ? (#.Register == "abc")') as reg_abc
from ofsted_items
) t
At one point or another you will have to explicitly write out one expression for each column, so jsonb_to_recordset doesn't really buy you that much.
Online example
If you need this a lot, you can easily put this into a view.
Try below query:
select id,name,
max(case when register='xyz' then true end) as "register_xyz?",
max(case when register='xyz' then age_from end) as xyz_age_from,
max(case when register='xyz' then age_to end) as xyz_age_to,
max(case when register='xyz' then maximum_number end) as xyz_maximum_number,
max(case when register='abc' then true end) as "register_abc?",
max(case when register='abc' then age_from end) as abc_age_from,
max(case when register='abc' then age_to end) as abc_age_to,
max(case when register='abc' then maximum_number end) as abc_maximum_number
from (SELECT
items.id,
items.name,
care_ages.*
FROM
ofsted_items,
jsonb_to_recordset(items.care_age) AS care_ages ("AgeFrom" integer, "AgeTo" integer, "Register" text, "MaximumNumber" integer)
)t
group by id, name
You can use conditional aggregation to pivot your table. This can be done using the CASE clause as it was done at the solution you already linked or using the FILTER clause:
demo:db<>fiddle
SELECT
id,
name,
bool_and(true) FILTER (WHERE register = 'xyz') as "register_xyz?",
MAX(age_from) FILTER (WHERE register = 'xyz') as xyz_age_from,
MAX(age_to) FILTER (WHERE register = 'xyz') as xyz_age_to,
MAX(maximum_number) FILTER (WHERE register = 'xyz') as xyz_maximum_number,
bool_and(true) FILTER (WHERE register = 'abc') as "register_abc?",
MAX(age_from) FILTER (WHERE register = 'abc') as abc_age_from,
MAX(age_to) FILTER (WHERE register = 'abc') as abc_age_to,
MAX(maximum_number) FILTER (WHERE register = 'abc') as abc_maximum_number
FROM items,
jsonb_to_recordset(items.care_ages) AS care_ages ("age_from" integer, "age_to" integer, "register" text, "maximum_number" integer)
GROUP BY id, name

"Transpose" of a table in Oracle

I'm having quite a bit of trouble figuring out exactly how to rearrange a table. I have a large table that looks something like this:
+--------+-----------+
| NAME | ACCOUNT # |
+--------+-----------+
| Nike | 87 |
| Nike | 12 |
| Adidas | 80 |
| Adidas | 21 |
+--------+-----------+
And I want to rearrange it to look like this:
+------+--------+
| Nike | Adidas |
+------+--------+
| 87 | 80 |
| 12 | 21 |
+------+--------+
But I can't seem to figure out how. I tried using PIVOT, but that only works with aggregate functions. I tried using a FOR LOOP as well, but couldn't get it work just right.
You can do this in several ways, but all being by enumerating the rows. Here is an example using conditional aggregation:
select max(case when name = 'Nike' then account end) as Nike,
max(case when name = 'Adidas' then account end) as Adidas
from (select t.*,
row_number() over (partition by name order by account desc) as seqnum
from t
) t
group by seqnum;
Consider again a pivot solution but first adding a rownumber for rolling Name group counts. Below assumes an autonumber ID field:
SELECT * FROM
(
SELECT Name, "Account #",
(ROW_NUMBER() OVER(PARTITION BY Name ORDER BY ID)) GrpRowNum
/* ALT: (SELECT Count(*) FROM Table1 sub
* WHERE sub.Name = Table1.Name AND sub.ID <= Table1.ID) GrpRowNum */
FROM Table1
)
PIVOT
(
SUM("Account #")
FOR Name IN ('Nike', 'Adidas')
)
ORDER BY RowNum;
However, for your ~200 items, you cannot easily render the Pivot's IN clause without various workarounds including PIVOT XML output or stored procedures with PL/SQL. Similarly, you could use general purpose coding (Java, PHP, Python, R) to retreive SELECT DISTINCT Name FROM Table1 resultset in vector/array, joining element values (collapsing or imploding arrays) with quotes and comma separators, and dropping the entire list in IN clause.