Postgresql join only most specific cidr match - postgresql

I have a table "test_networks" that is a list of networks with a description about what each network is and where it is located.
CREATE TABLE test_networks
(
id serial PRIMARY KEY,
address cidr,
description text
);
The field "address" will be any of the following:
10.0.0.0/8
10.1.0.0/16
10.1.1.0/24
10.2.0.0/16
10.3.0.0/16
10.3.1.0/24
10.3.2.0/24
10.3.3.0/24
10.15.1.0/24
10.15.2.0/24
10.15.3.0/24
I also have a table "test_systems" which contains a list of systems and their properties (I have a few more properties, but those are irrelevant):
CREATE TABLE test_systems
(
id serial PRIMARY KEY,
address inet,
owner text
);
Lets assume I have systems with the following addresses:
10.1.1.1
10.2.0.1
I want to create a report of all systems and their closest networks description (or empty description if no network is found). As you can see, 10.1.1.1 matches multiple networks, so I only want to list the most specific one (i.e. the one with the highest masklen()) for each system. Example output would be:
hostaddr | netaddr | description
----------+-------------+----------------
10.1.1.1 | 10.1.1.0/24 | third network
10.2.0.1 | 10.2.0.0/16 | 4th network
I tried using this query:
SELECT s.address AS hostaddr, n.address AS netaddr, n.description AS description
FROM test_systems s
LEFT JOIN test_networks n
ON s.address << n.address;
However, this will give me a list of all system + network pairs, e.g.:
hostaddr | netaddr | description
----------+-------------+----------------
10.1.1.1 | 10.0.0.0/8 | first network
10.1.1.1 | 10.1.0.0/16 | second network
10.1.1.1 | 10.1.1.0/24 | third network
10.2.0.1 | 10.0.0.0/8 | first network
10.2.0.1 | 10.2.0.0/16 | 4th network
Does anyone know how I can query for only the most specific network for each system?

You're looking for the "top n in group" query, where n = 1 in this case. You can do this using the row_number() window function:
SELECT x.hostaddr, x.netaddr, x.description FROM (
SELECT
s.address AS hostaddr,
n.address AS netaddr,
n.description AS description,
row_number() OVER (
PARTITION BY s.address
ORDER BY masklen(n.address) DESC
) AS row
FROM test_systems s
LEFT JOIN test_networks n
ON s.address << n.address
) x
WHERE x.row = 1;

SELECT distinct on (s.address)
s.address AS hostaddr,
n.address AS netaddr,
n.description AS description
FROM
test_systems s
LEFT JOIN
test_networks n ON s.address << n.address
order by s.address, masklen(n.address) desc

Related

Postgres: Query for list of ids in a mapping table and create If they don't exist

Assume we have the following table whose purpose is to autogenerate a numeric id for distinct (name, location) tuples:
CREATE TABLE mapping
(
id bigserial PRIMARY KEY,
name text NOT NULL,
location text NOT NULL,
);
CREATE UNIQUE INDEX idx_name_loc on mapping(name location)
What is the most efficient way to query for a set of (name, location) tuples and autocreate any mappings that don't already exist, with all mappings (including the ones we created) being returned to the user.
My naive implementation would be something like:
SELECT id, name, location
FROM mappings
WHERE (name, location) IN ((name_1, location_1)...(name_n, location_n))
do something with the results in a programming language of may choice to work out which results are missing.
INSERT
INTO mappings (name, location)
VALUES (missing_name_1, missing_loc_1), ... (missing_name_2, missing_loc_2)
ON CONFLICT DO NOTHING
This gets the job done but I get the feeling there's probably something that can a) be done in pure sql and b) is more efficient.
You can use DISTINCT to get all possible values for the two columns, and CROSS JOIN to get their Carthesian product.
LEFT JOIN with the original table to get the actual records (if any):
CREATE TABLE mapping
( id bigserial PRIMARY KEY
, name text NOT NULL
, location text NOT NULL
, UNIQUE (name, location)
);
INSERT INTO mapping(name, location) VALUES ('Alice', 'kitchen'), ('Bob', 'bedroom' );
SELECT * FROM mapping;
SELECT n.name, l.location, m.id
FROM (SELECT DISTINCT name from mapping) n
CROSS JOIN (SELECT DISTINCT location from mapping) l
LEFT JOIN mapping m ON m.name = n.name AND m.location = l.location
;
Results:
DROP SCHEMA
CREATE SCHEMA
SET
CREATE TABLE
INSERT 0 2
id | name | location
----+-------+----------
1 | Alice | kitchen
2 | Bob | bedroom
(2 rows)
name | location | id
-------+----------+----
Alice | kitchen | 1
Alice | bedroom |
Bob | kitchen |
Bob | bedroom | 2
(4 rows)
And if you want to physically INSERT the missing combinations:
INSERT INTO mapping(name, location)
SELECT n.name, l.location
FROM (SELECT DISTINCT name from mapping) n
CROSS JOIN (SELECT DISTINCT location from mapping) l
WHERE NOT EXISTS(
SELECT *
FROM mapping m
WHERE m.name = n.name AND m.location = l.location
)
;
SELECT * FROM mapping;
INSERT 0 2
id | name | location
----+-------+----------
1 | Alice | kitchen
2 | Bob | bedroom
3 | Alice | bedroom
4 | Bob | kitchen
(4 rows)

Transforming information in postgresql

So, I have 2 tables,
In the 1st table, there is an Information of users
user_id | name
1 | Albert
2 | Anthony
and in the other table, I have information
where some users have address information where it can either be home, office or both home and office
user_id| address_type | address
1 | home | a
1 | office | b
2 | home | c
and the final result I want is this
user_id | name | home_address | office_address
1 | Albert | a | b
2 | Anthony | c | null
I have tried using left join and json_agg but the information that way is not readable,
any suggestions on how I can do this?
You can use two outer joins, one for the office address and one for the home address.
select t1.user_id, t1.name,
ha.address as home_address,
oa.address as office_address
from table1 t1
left join table2 ha on ha.user_id = t1.user_id and ha.address_type = 'home'
left join table2 oa on oa.user_id = t1.user_id and ha.address_type = 'office';
A solution using JSON could look like this
select t1.user_id, t1.name,
a.addresses ->> 'home' as home_address,
a.addresses ->> 'office' as office_address
from table1 t1
left join (
select user_id, jsonb_object_agg(address_type, address) as addresses
from table2
group by user_id
) a on a.user_id = t1.user_id;
Which might be a bit more flexible, because you don't need to add a new join for each address type. The first query is likely to be faster if you need to retrieve a large number of rows.

PostgreSQL: Selecting one address from almost but not exactly duplicate rows

I have a big table that I'm trying to join another table to, however the table has entries such as:
--- Name | Address | Priority
----------------------------------------
1 | Jane Doe | 123 Baker St | 1
2 | Jane Doe | 345 Clay Dr | 2
3 | Jeff Boe | 231 Street St| 1
4 | Karen Al | 4232 Elm St | 1
5 | Karen Al | 5632 Pine Ct | 2
What I really want to select is one single address per person. The correct address I want is priority 2. However some of the addresses don't have a priority 2, so I can't join only on priority 2.
I've tried the following test query:
SELECT DISTINCT n.ID, LastName, FirstName, MAX(Address), MAX(Address2), City, State, PostalCode, n.Phone
FROM NormalTable n
JOIN Contracts cn ON n.ID = cn.ID
Which returns the table that I sketched out above, with the same person/sameID but different addresses.
Is there a way to do this in one query? I can think of maybe doing one INSERT statement into my final table where I do all the priority 2 addresses and then ANOTHER INSERT statement for IDs that aren't in the table yet, and use the priority 1 address for those. But I'd much prefer if there's a way to do this all in one go where I end up with only the address I want.
You could choice the address you need joining a subquery for max priority
select m.LastName, m.FirstName, m.Address, m.Address2, m.City, m.State, m.PostalCode, m.Phone
from my_table m
inner join (
select LastName, FirstName, max(priority) max_priority
from my_table
group by LastName, FirstName
) t on t.LastName = m.LastName
AND t.FirstName = m.FirstName
AND t.max_priority = m.priority
I think you want something like this
SELECT DISTINCT (Name), Address, Priority
ORDER BY Priority DESC
How this works is that the DISTINCT (Name) only returns one row per name. The row returned for each Name is the first row. Which will be the one with the highest priority because of the ORDER BY.

Select common elements of an array of ids Postgresql

So i have this task to resolve in Postgresql where for a given number of classIds that the user provides i have to return him the common properties of said classes.
I have three tables to represent the model (one class has multiple properties and one property can be in many classes)
Table classes:
---------------------------
| Id | Name | Description |
---------------------------
Table Properties:
---------------------------
| Id | Name | Description |
---------------------------
And finally table ClassProperties
------------------------
| ClassId | PropertyId |
------------------------
So the users gives me an array with classIds and i have to return him all common properties of all classes (like i said above)
As of now i'm only being able to return every property of all classes with this code:
select p.*
from properties as p
inner join ClassProperties as cp on cp.propertyid= p.id
where cp.classid = any ('{88d5fe8f-e19e-40b4-bc65-83ac64f825b0, a2a63bea-
aeee-4d3b-817e-cc635383c571}') ;
The ids as u can see are Guid, but that really does't matter. Any help would be appreciated. Thanks!
You can do this as:
select p.*
from properties p inner join
ClassProperties cp
on cp.propertyid = p.id
where cp.classid = any ('{88d5fe8f-e19e-40b4-bc65-83ac64f825b0, a2a63bea-aeee-4d3b-817e-cc635383c571}')
group by p.id
having count(*) = 2;
The count(*) should be count(distinct classid) if duplicates are allowed.
Note: the value "2" is the number of elements you are checking. You can also use array_length('{88d5fe8f-e19e-40b4-bc65-83ac64f825b0, a2a63bea-aeee-4d3b-817e-cc635383c571}').

Getting Breadcrumbs in Postgres

I have a table that represents a hierarchy by referencing itself.
create table nodes (
id integer primary key,
parent_id integer references nodes (id),
name varchar(255)
);
Given a specific node, I would like to find all of its parents in order, as breadcrumbs. For example, given this data:
insert into nodes (id,parent_id,name) values
(1,null,'Root'),
(2,1,'Left'),
(3,1,'Right'),
(4,2,'LeftLeft'),
(5,2,'LeftRight'),
(6,5,'LeftRightLeft');
If I wanted to start at id=5 I would expect the result to be:
id | depth | name
-- | ----- | ----
1 | 0 | 'Root'
2 | 1 | 'Left'
5 | 2 | 'LeftRight'
I don't care if the depth column is present, but I included it for clarity, to show that there should only be one result for each depth and that results should be in order of depth. I don't care if it's ascending or descending. The purpose of this is to be able to print out some breadcrumbs that look like this:
(1)Root \ (2)Left \ (5)LeftRight
The basic recursive query would look like this:
with recursive tree(id, name, parent_id) as (
select n.id, n.name, n.parent_id
from nodes n
where n.id = 5
union all
select n.id, n.name, n.parent_id
from nodes n
join tree t on (n.id = t.parent_id)
)
select *
from tree;
Demo: http://sqlfiddle.com/#!15/713f8/1
That will give you everything need to rebuild the path from id = 5 back to the root.