How to format sql table using values as column - postgresql

I have the following result :
-------------------------
| dept | Active request |
-------------------------
| AFG | 3 |
| AGO | 4 |
| KMN | 1 |
| MOL | 1 |
| POD | 2 |
| SUD | 2 |
-------------------------
How can I tranform it to have something like
--------------------------------------------------------------
| Title | AFG | AGO | KMN | MOL | POD | SUD | TOTAL |
--------------------------------------------------------------
| Active Request | 3 | 4 | 1 | 1 | 2 | 2 | 13 |
--------------------------------------------------------------
Here is my fiddle http://sqlfiddle.com/#!9/b51a03/3

You could use a single pivot query:
SELECT
'Active Request' AS Title,
COUNT(*) FILTER (WHERE dept = 'AFG') AS AFG,
COUNT(*) FILTER (WHERE dept = 'AGO') AS AGO,
COUNT(*) FILTER (WHERE dept = 'KMN') AS KMN,
COUNT(*) FILTER (WHERE dept = 'MOL') AS MOL,
COUNT(*) FILTER (WHERE dept = 'POD') AS POD,
COUNT(*) FILTER (WHERE dept = 'SUD') AS SUD,
COUNT(*) AS TOTAL
FROM req
WHERE active;

This can be done with the postgresql crosstab tablefunc.
You can find an detailed explanation at: Pivot Tables in PostgreSQL Using the Crosstab Function

Related

How to use a function inside a filter

I'm extracting some data between () and I want to use the extrated data as column. Here is my fiddle https://www.db-fiddle.com/f/hY1JFUwk3YNGYye345pny8/2
My base table :
----------------------------
| id | active | dept |
----------------------------
| 1 | true | TEST (AFG) |
| 2 | true | TEST (AFG) |
| 3 | true | TEST (AFG) |
| 4 | true | TEST (POD) |
| 5 | true | TEST (POD) |
| 6 | true | TEST (KMN) |
| 7 | true | TEST (AGO) |
| 8 | true | TEST (AGO) |
| 9 | false | TEST (AGO) |
| 10 | true | TEST (AGO) |
| 11 | true | TEST (AGO) |
| 12 | true | TEST (SUD) |
| 13 | true | TEST (SUD) |
| 14 | true | TEST (MOL) |
----------------------------
My current request (retreive active and inactive request):
SELECT
'Active Request' AS Title,
(regexp_matches(dept, '\((.*?)\)'))[1] as dept,
COUNT(*) FILTER (WHERE dept = 'AFG') AS AFG,
COUNT(*) FILTER (WHERE dept = 'AGO') AS AGO,
COUNT(*) FILTER (WHERE dept = 'KMN') AS KMN,
COUNT(*) FILTER (WHERE dept = 'MOL') AS MOL,
COUNT(*) FILTER (WHERE dept = 'POD') AS POD,
COUNT(*) FILTER (WHERE dept = 'SUD') AS SUD,
COUNT(*) AS TOTAL
FROM req
WHERE active = 'true'
GROUP BY dept
UNION
SELECT
'Inactive Request' AS Title,
(regexp_matches(dept, '\((.*?)\)'))[1] as dept,
COUNT(*) FILTER (WHERE dept = 'AFG') AS AFG,
COUNT(*) FILTER (WHERE dept = 'AGO') AS AGO,
COUNT(*) FILTER (WHERE dept = 'KMN') AS KMN,
COUNT(*) FILTER (WHERE dept = 'MOL') AS MOL,
COUNT(*) FILTER (WHERE dept = 'POD') AS POD,
COUNT(*) FILTER (WHERE dept = 'SUD') AS SUD,
COUNT(*) AS TOTAL
FROM req
WHERE active = 'false'
GROUP BY dept;
The issue is I can't use the regexp into my filter.
Using the sql request :
SELECT
'Active Request' AS Title,
COUNT(*) FILTER (WHERE (regexp_matches(dept, '\((.*?)\)'))[1] = 'AFG') AS AFG,
COUNT(*) FILTER (WHERE (regexp_matches(dept, '\((.*?)\)'))[1] = 'AGO') AS AGO,
COUNT(*) FILTER (WHERE (regexp_matches(dept, '\((.*?)\)'))[1] = 'KMN') AS KMN,
COUNT(*) FILTER (WHERE (regexp_matches(dept, '\((.*?)\)'))[1] = 'MOL') AS MOL,
COUNT(*) FILTER (WHERE (regexp_matches(dept, '\((.*?)\)'))[1] = 'POD') AS POD,
COUNT(*) FILTER (WHERE (regexp_matches(dept, '\((.*?)\)'))[1] = 'SUD') AS SUD,
COUNT(*) AS TOTAL
FROM req
WHERE active = 'true'
GROUP BY dept;
Will throw me the following error :
Query Error: error: set-returning functions are not allowed in FILTER
My goal is to retreive data like :
----------------------------------------------------------------
| Title | AFG | AGO | KMN | MOL | POD | SUD | TOTAL |
----------------------------------------------------------------
| Active Request | 3 | 4 | 1 | 1 | 2 | 2 | 13 |
| Inactive Request | 0 | 1 | 0 | 0 | 0 | 0 | 1 |
----------------------------------------------------------------
At this moment I'm able to extract the dept between () using (regexp_matches(dept, '\((.*?)\)'))[1] however, I can't use it in a filter statment
Not sure if this helps, you could pull the regexp out to a CTE and filter on that, not sure if it's the most efficient approach but may get you started:
WITH dept_codes AS (
SELECT
dept,
active,
(regexp_matches(dept, '\((.*?)\)'))[1] AS code
FROM
req
)
SELECT
'Active Request' AS Title,
COUNT(*) FILTER (WHERE code = 'AFG') AS AFG,
COUNT(*) FILTER (WHERE code = 'AGO') AS AGO,
COUNT(*) FILTER (WHERE code = 'KMN') AS KMN,
COUNT(*) FILTER (WHERE code = 'MOL') AS MOL,
COUNT(*) FILTER (WHERE code = 'POD') AS POD,
COUNT(*) FILTER (WHERE code = 'SUD') AS SUD,
COUNT(*) AS TOTAL
FROM
dept_codes
WHERE
active = 'true';
Returns
title | afg | ago | kmn | mol | pod | sud | total
----------------+-----+-----+-----+-----+-----+-----+-------
Active Request | 3 | 4 | 1 | 1 | 2 | 2 | 13
If you want to include inactive results, of course you could do something like this:
WITH dept_codes AS (
SELECT
dept,
active,
(regexp_matches(dept, '\((.*?)\)'))[1] AS code
FROM
req
)
SELECT
CASE WHEN active = 'true' THEN 'Active Request' ELSE 'Inactive Request' END AS Title,
COUNT(*) FILTER (WHERE code = 'AFG') AS AFG,
COUNT(*) FILTER (WHERE code = 'AGO') AS AGO,
COUNT(*) FILTER (WHERE code = 'KMN') AS KMN,
COUNT(*) FILTER (WHERE code = 'MOL') AS MOL,
COUNT(*) FILTER (WHERE code = 'POD') AS POD,
COUNT(*) FILTER (WHERE code = 'SUD') AS SUD,
COUNT(*) AS TOTAL
FROM
dept_codes
GROUP BY 1;
Which returns
title | afg | ago | kmn | mol | pod | sud | total
------------------+-----+-----+-----+-----+-----+-----+-------
Active Request | 3 | 4 | 1 | 1 | 2 | 2 | 13
Inactive Request | 0 | 1 | 0 | 0 | 0 | 0 | 1
(2 rows)

PostgreSQL - Check if column value exists in any previous row

I'm working on a problem where I need to check if an ID exists in any previous records within another ID set, and create a tag if it does.
Suppose I have the following table
| client_id | order_date | supplier_id |
| 1 | 2022-01-01 | 1 |
| 1 | 2022-02-01 | 2 |
| 1 | 2022-03-01 | 1 |
| 1 | 2022-04-01 | 3 |
| 2 | 2022-05-01 | 1 |
| 2 | 2022-06-01 | 1 |
| 2 | 2022-07-01 | 2 |
And I want to create a column with a "is new supplier" tag (for each client):
| client_id | order_date | supplier_id | is_new_supplier|
| 1 | 2022-01-01 | 1 | True
| 1 | 2022-02-01 | 2 | True
| 1 | 2022-03-01 | 1 | False
| 1 | 2022-04-01 | 3 | True
| 2 | 2022-05-01 | 1 | True
| 2 | 2022-06-01 | 1 | False
| 2 | 2022-07-01 | 2 | True
First I tried doing this by creating a dense_rank and filtering out repeated ranks, but it didn't work:
with aux as (SELECT client_id,
order_date,
supplier_id
FROM table)
SELECT *, dense_rank() over (
partition by client_id
order by supplier_id
) as _dense_rank
FROM aux
Another way I thought about doing this, is by creating an auxiliary id with client_id + supplier_id, ordering by date and checking if the aux id exists in any previous row, but I don't know how to do this in SQL.
You are on the right track.
Instead of dense_rank, you can just use row_number and on your partition by add supplier id..
Don't forget to order by order_date
with aux as (SELECT client_id,
order_date,
supplier_id,
row_number() over (
partition by client_id, supplier_id
order by order_date
) as rank
FROM table)
SELECT client_id,
order_date,
supplier_id,
rank,
(rank = 1) as is_new_supplier
FROM aux

SUM values from two tables with GROUP BY and WHERE

I have two tables below named sent_table and received_table. I am attempting to mash them together in a query to achieve output_table. All my attempts so far result in a huge amount of duplicates and totally bogus sum values.
I am assuming I would need to use GROUP BY and WHERE to achieve this goal. I want to be able to filter based on the users name.
sent_table
+----+------+-------+----------+
| id | name | value | order_id |
+----+------+-------+----------+
| 1 | dave | 100 | 1 |
| 2 | dave | 200 | 1 |
| 3 | dave | 300 | 2 |
+----+------+-------+----------+
received_table
+----+------+-------+----------+
| id | name | value | order_id |
+----+------+-------+----------+
| 1 | dave | 400 | 1 |
| 2 | dave | 500 | 2 |
| 3 | dave | 600 | 2 |
+----+------+-------+----------+
output table
+------+----------+----------+
| sent | received | order_id |
+------+----------+----------+
| 300 | 400 | 1 |
| 300 | 1100 | 2 |
+------+----------+----------+
I tried the following with no joy. This does not impose any restrictions on how I would desire to solve this problem. It is just how I attempted to do it.
SELECT *
FROM
( select SUM(value) as sent, order_id FROM sent_table WHERE name='dave' GROUP BY order_id) A
CROSS JOIN
( select SUM(value) as received, order_id FROM received_table WHERE name='dave' GROUP BY order_id) B
Any help would be greatly appreciated.
Do the sums on each table, grouping by order_id, then join the results. To get the rows even if one side is missing, do a FULL OUTER JOIN:
SELECT COALESCE(s.order_id, r.order_id) AS order_id, s.sent, r.received
FROM (
SELECT order_id, SUM(value) AS sent
FROM sent
GROUP BY order_id
) s
FULL OUTER JOIN (
SELECT order_id, SUM(value) AS received
FROM received
GROUP BY order_id
) r
USING (order_id)
ORDER BY 1
Result:
| order_id | sent | received |
| -------- | ---- | -------- |
| 1 | 300 | 400 |
| 2 | | 1100 |
Note the COALESCE on the order_id, so that if it's missing from sent it will be taken from recevied, so that that value will never be NULL.
If you want to have 0 in place of NULL (when e.g. there is no record for that order_id in either sent or received), you would do COALESCE(s.sent, 0) AS sent, COALESCE(r.received, 0) AS received.
https://www.db-fiddle.com/f/nq3xYrcys16eUrBRHT6xLL/2

1th and 7th row in grouping

I have this table named Samples. The Date column values are just symbolic date values.
+----+------------+-------+------+
| Id | Product_Id | Price | Date |
+----+------------+-------+------+
| 1 | 1 | 100 | 1 |
| 2 | 2 | 100 | 2 |
| 3 | 3 | 100 | 3 |
| 4 | 1 | 100 | 4 |
| 5 | 2 | 100 | 5 |
| 6 | 3 | 100 | 6 |
...
+----+------------+-------+------+
I want to group by product_id such that I have the 1'th sample in descending date order and a new colomn added with the Price of the 7'th sample row in each product group. If the 7'th row does not exist, then the value should be null.
Example:
+----+------------+-------+------+----------+
| Id | Product_Id | Price | Date | 7thPrice |
+----+------------+-------+------+----------+
| 4 | 1 | 100 | 4 | 120 |
| 5 | 2 | 100 | 5 | 100 |
| 6 | 3 | 100 | 6 | NULL |
+----+------------+-------+------+----------+
I belive I can achieve the table without the '7thPrice' with the following
SELECT * FROM (
SELECT ROW_NUMBER() OVER (PARTITION BY Product_Id ORDER BY date DESC) r, * FROM Samples
) T WHERE T.r = 1
Any suggestions?
You can try something like this. I used your query to create a CTE. Then joined rank1 to rank7.
;with sampleCTE
as
(SELECT ROW_NUMBER() OVER (PARTITION BY Product_Id ORDER BY date DESC) r, * FROM Samples)
select *
from
(select * from samplecte where r = 1) a
left join
(select * from samplecte where r=7) b
on a.product_id = b.product_id

Grouping in t-sql with latest dates

I have a table like this
Event ID | Contract ID | Event date | Amount |
----------------------------------------------
1 | 1 | 2009-01-01 | 100 |
2 | 1 | 2009-01-02 | 20 |
3 | 1 | 2009-01-03 | 50 |
4 | 2 | 2009-01-01 | 80 |
5 | 2 | 2009-01-04 | 30 |
For each contract I need to fetch the latest event and amount associated with the event and get something like this
Event ID | Contract ID | Event date | Amount |
----------------------------------------------
3 | 1 | 2009-01-03 | 50 |
5 | 2 | 2009-01-04 | 30 |
I can't figure out how to group the data correctly. Any ideas?
Thanks in advance.
SQL 2k5/2k8:
with cte_ranked as (
select *
, row_number() over (
partition by ContractId order by EvantDate desc) as [rank]
from [table])
select *
from cte_ranked
where [rank] = 1;
SQL 2k:
select t.*
from table as t
join (
select max(EventDate) as MaxDate
, ContractId
from table
group by ContractId) as mt
on t.ContractId = mt.ContractId
and t.EventDate = mt.MaxDate