Get rows that no foreign keys point to - sql

I have two tables
CREATE TABLE public.city_url
(
id bigint NOT NULL DEFAULT nextval('city_url_id_seq'::regclass),
url text,
city text,
state text,
country text,
common_name text,
CONSTRAINT city_url_pkey PRIMARY KEY (id)
)
and
CREATE TABLE public.email_account
(
id bigint NOT NULL DEFAULT nextval('email_accounts_id_seq'::regclass),
email text,
password text,
total_replied integer DEFAULT 0,
last_accessed timestamp with time zone,
enabled boolean NOT NULL DEFAULT true,
deleted boolean NOT NULL DEFAULT false,
city_url_id bigint,
CONSTRAINT email_accounts_pkey PRIMARY KEY (id),
CONSTRAINT email_account_city_url_id_fkey FOREIGN KEY (city_url_id)
REFERENCES public.city_url (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)
I want to come up with a query that fetches rows in the city_url only if there is no row in the email_account pointing to it with the city_url_id column.

NOT EXISTS comes to mind:
select c.*
from city_url c
where not exists (select 1
from email_account ea
where ea.city_url_id = c.id
);

There's also this option:
SELECT city_url.*
FROM city_url
LEFT JOIN email_account ON email_account.city_url_id = city_url.id
WHERE email_account.id IS NULL

A NOT EXISTS is absolutely the answer to the "... if there is no row ...".
Nonetheless it would be preferable to accomplish this by selecting then difference quantity.
Which is in principle:
SELECT a.*
FROM table1 a
LEFT JOIN table2 b
ON a.[columnX] = b.[columnY]
WHERE b.[columnY] IS NULL
Using the tablenames here, this would be:
SELECT c.*
FROM city_url c
LEFT JOIN email_account e
ON c.id = e.city_url
WHERE e.city_url IS NULL

I believe NOT IN could be used here as well, although this might be less performant on large datasets:
SELECT *
FROM city_url
WHERE id NOT IN (
SELECT city_url_id FROM email_account
)

Related

Join two tables so foreign key column data (integer) changes to text data from parent table (concerned column in parent table is not primary key)

I am a beginner with SQLite, and I am still a little unfamiliar with joining tables (and the limitations therein). I would like to know how to join 2 tables so that the column data in Table B (several columns) changes to reflect data from Table A.
CREATE TABLE "A" (
"person_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"person_name" TEXT NOT NULL,
"email" TEXT NOT NULL
);
CREATE TABLE "B" (
"company_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"company_name" TEXT NOT NULL,
"contact_one" INTEGER NOT NULL,
"contact_two" INTEGER,
"contact_three" INTEGER,
FOREIGN KEY("contact_one") REFERENCES "A"("person_id") ON DELETE SET NULL
FOREIGN KEY("contact_two") REFERENCES "A"("person_id") ON DELETE SET NULL
FOREIGN KEY("contact_three") REFERENCES "A"("person_id") ON DELETE SET NULL
);
How do I write a query so that the resulting table shows columns "company_name", "contact_one", "contact_two" and "contact_three" BUT with the contact tables showing the contact name rather than the integer (as it would appear in Table B).
See below an image representing Tables A and B and the desired output of SQLite query:
When I do a left join, the query succeeds only for a single contact column.
SELECT B.company_name, A.person_name
FROM B
LEFT JOIN A ON B.contact_one = A.person_id
I tried to add an "OR" to the query (I know "AND" will not work), but I get an "ambiguous column name: A.person_name" error when I try to run the query:
SELECT B.company_name, A.person_name
FROM B
LEFT JOIN A ON B.contact_one = A.person_id
OR LEFT JOIN A ON B.contact_two = A.person_id
OR LEFT JOIN A ON B.contact_three = A.person_id
How do I write the query so that I can get contact_two and contact_three also in the resulting table, with all three contacts' names displayed?
Any guidance will be greatly appreciated!
You need to join the table A 3 times like this:
SELECT B.company_name,
a1.person_name name1,
a2.person_name name2,
a3.person_name name3
FROM B
LEFT JOIN A a1 ON B.contact_one = a1.person_id
LEFT JOIN A a2 ON B.contact_two = a2.person_id
LEFT JOIN A a3 ON B.contact_three = a3.person_id

postgres: (sub)select and combine optional content into an array

i have the following table structure:
Location----- * Media ----1 Attribute --------* AttributeTranslation
Each Location has n mediaitems attached, containing one optional attribute (text) and n associated translationa for that attribute.
I need to select this data into an array, so that i get for each location the associated medialist for each language.
what i currently do and what i get:
SELECT m.location_id, t.language_id,
array_agg_mult(
ARRAY[ARRAY[m.sortorder::text, m.filename, t.name]] ORDER BY m.sortorder
) as medialist
FROM Media m
LEFT JOIN ATTRIBUTE a ON a.id = m.attribute_id
LEFT JOIN AttributeTranslation t ON a.id = t.attribute_id
WHERE m.location_id = ?
GROUP BY m.location_id, t.language_id
This gives me following result for the given scenario: the current location has 4 images attached, only the first image has an associated attribute containing two translations:
Location_ID Language_ID MEDIALIST
AT_014 1 {{1,'location_image1.jpg','attribute german'}}
AT_014 2 {{1,'location_image1.jpg','attribute english'}}
AT_014 {{2,'location_image2.jpg',null},{3,'location_image3.jpg',null},{4,'location_image4.jpg',null}}
but what i need instead is this:
Location_ID Language_ID MEDIALIST
AT_014 1 {{1,'location_image1.jpg','attribute german'},{2,'location_image2.jpg',null},{3,'location_image3.jpg',null},{4,'location_image4.jpg',null}}
AT_014 2 {{1,'location_image1.jpg','attribute english'},{2,'location_image2.jpg',null},{3,'location_image3.jpg',null},{4,'location_image4.jpg',null}}
those 3 columns are part of a view, so that i can do later:
select * from locationview where location_id = ? and language_id = ?
how can i achieve the desired result here? thanks in advance!
Simplified Table Definitions:
CREATE TABLE LOCATION (
location_id numeric(20) primary key,
description text
);
CREATE TABLE MEDIA (
media_id numeric(20) primary key,
fileName text,
sortorder smallint,
location_id numeric(20) references LOCATION(location_id),
attribute_id numeric(20) references ATTRIBUTE(attribute_id)
);
CREATE TABLE ATTRIBUTE (
attribute_id numeric(20) primary key,
attributetype varchar(100),
);
CREATE TABLE ATTRIBUTETRANSLATION (
translation_id numeric(20),
language_id smallint,
name text,
description text,
attribute_id numeric(20) references ATTRIBUTE(attribute_id)
);
ALTER TABLE ATTRIBUTETRANSLATION add constraint AT_ID primary key(translation_id, language_id)
I am not sure I fully understand your question, but here's an attempt. You could take the output of your query, and match each row that has a language_id with the corresponding rows where language_id is NULL, so that you can then concatenate the medialist arrays. Here's a way to do that by creating an alias of your query with a CTE:
WITH t AS (
SELECT m.location_id, t.language_id,
array_agg(
ARRAY[ARRAY[m.sortorder::text, m.filename, t.name]] ORDER BY m.sortorder
) as medialist
FROM Media m
LEFT JOIN ATTRIBUTE a ON a.attribute_id = m.attribute_id
LEFT JOIN AttributeTranslation t ON a.attribute_id = t.attribute_id
WHERE m.location_id = ?
GROUP BY m.location_id, t.language_id
)
SELECT location_id, t1.language_id, t1.medialist || t2.medialist AS medialist
FROM (SELECT * FROM t WHERE language_id IS NOT NULL) t1
RIGHT OUTER JOIN (SELECT * FROM t WHERE language_id IS NULL) t2 USING (location_id);
I am not sure if this does exactly what you want, but hopefully it will give you some ideas.

Role basesd access control recursive SQL query

I made the following role based access database schema which is able to hold roles, operations and types. A role can perform a specific operation on a type. Connections to users or types is not important here because this will be application specific. Every of these three tables can have as many parents as they want.
At the moment I'm struggling with a query which outputs every possible combination from the role_operation_type table.
Every role should inherit every permission on a types from the ancestors which can be more than one. In my opinion I need three nested recursive with queries for that or is there any faster way to achieve that?
My intention is to put that query in a view and select the needed values when a user requests an operation on a type.
Here is the database schema:
CREATE TABLE IF NOT EXISTS `role` (
`id` INTEGER PRIMARY KEY,
`name` VARCHAR NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS `role_role` (
`role_id` INTEGER NOT NULL REFERENCES `role`(`id`) ON UPDATE CASCADE ON DELETE CASCADE,
`parent_role_id` INTEGER NOT NULL REFERENCES `role`(`id`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT role_uq UNIQUE (`role_id`, `parent_role_id`),
CONSTRAINT role_chk CHECK(`role_id` != `parent_role_id`)
);
CREATE TABLE IF NOT EXISTS `operation` (
`id` INTEGER PRIMARY KEY,
`name` VARCHAR NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS `operation_operation` (
`operation_id` INTEGER NOT NULL REFERENCES `operation`(`id`) ON UPDATE CASCADE ON DELETE CASCADE,
`parent_operation_id` INTEGER NOT NULL REFERENCES `operation`(`id`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT operation_uq UNIQUE (`operation_id`, `parent_operation_id`),
CONSTRAINT operation_chk CHECK(`operation_id` != `parent_operation_id`)
);
CREATE TABLE IF NOT EXISTS `type` (
`id` INTEGER PRIMARY KEY,
`name` VARCHAR NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS `type_type` (
`type_id` INTEGER NOT NULL REFERENCES `type`(`id`) ON UPDATE CASCADE ON DELETE CASCADE,
`parent_type_id` INTEGER NOT NULL REFERENCES `type`(`id`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT type_uq UNIQUE (`type_id`, `parent_type_id`),
CONSTRAINT type_chk CHECK(`type_id` != `parent_type_id`)
);
CREATE TABLE IF NOT EXISTS `role_operation_type` (
`role_id` INTEGER NOT NULL REFERENCES `role`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
`operation_id` INTEGER NOT NULL REFERENCES `operation`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
`type_id` INTEGER NOT NULL REFERENCES `type`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT role_id_operation_id_type_id_uq UNIQUE (`role_id`, `operation_id`, `type_id`)
);
CREATE VIEW IF NOT EXISTS role_role_recursive_view AS
WITH RECURSIVE p(role_id, r, parent_role_id) AS (
SELECT ROLE.id, ROLE.id, role_role.parent_role_id
FROM ROLE
INNER JOIN role_role ON ROLE.id = role_role.role_id
UNION
SELECT p.r, p.role_id, role_role.parent_role_id
FROM p
INNER JOIN role_role ON p.parent_role_id = role_role.role_id
WHERE p.r != role_role.parent_role_id
)
SELECT p.role_id, p.parent_role_id FROM p ORDER BY role_id;
CREATE VIEW IF NOT EXISTS operation_operation_recursive_view AS
WITH RECURSIVE o(operation_id, o, parent_operation_id) AS (
SELECT operation.id, operation.id, operation_operation.parent_operation_id
FROM operation
INNER JOIN operation_operation ON operation.id = operation_operation.operation_id
UNION
SELECT o.o, o.operation_id, operation_operation.parent_operation_id
FROM o
INNER JOIN operation_operation ON o.parent_operation_id = operation_operation.operation_id
WHERE o.o != operation_operation.parent_operation_id
)
SELECT o.operation_id, o.parent_operation_id FROM o ORDER BY operation_id;
CREATE VIEW IF NOT EXISTS type_type_recursive_view AS
WITH RECURSIVE t(type_id, t, parent_type_id) AS (
SELECT TYPE.id, TYPE.id, type_type.parent_type_id
FROM TYPE
INNER JOIN type_type ON TYPE.id = type_type.type_id
UNION
SELECT t.t, t.type_id, type_type.parent_type_id
FROM t
INNER JOIN type_type ON t.parent_type_id = type_type.type_id
WHERE t.t != type_type.parent_type_id
)
SELECT t.type_id, t.parent_type_id FROM t ORDER BY type_id;
Now I will answer to my own question with a solution which works quite well. This is a recursive query and generates every possible combination. This is a recursive query which might be slow when the inheritance level becomes deeper.
CREATE VIEW IF NOT EXISTS role_operation_type_recursive_view AS
WITH RECURSIVE T(role_id, operation_id, type_id) AS (
WITH RECURSIVE O(role_id, operation_id, type_id) AS (
WITH RECURSIVE R(role_id, operation_id, type_id) AS (
SELECT role_id, operation_id,type_id
FROM role_operation_type
UNION
SELECT role_role_recursive_view.role_id, R.operation_id, R.type_id
FROM R
INNER JOIN role_role_recursive_view ON R.role_id = role_role_recursive_view.parent_role_id
)
SELECT * FROM R
UNION
SELECT O.role_id,operation_operation_recursive_view.parent_operation_id ,O.type_id
FROM O
INNER JOIN operation_operation_recursive_view ON O.operation_id = operation_operation_recursive_view.operation_id
)
SELECT * FROM O
UNION
SELECT T.role_id, T.operation_id, type_type_recursive_view.type_id
FROM T
INNER JOIN type_type_recursive_view ON T.type_id = type_type_recursive_view.parent_type_id
)
SELECT * FROM T;

get rows if all foreign rows are inactive

I have the following two tables. An "Account" can can many "Ads"
An account can have many ads.
I want to come up with a query that returns: any account that does not have any ads with the column active set to true.
If the account does not have any ads, then the query should not return it. And if only one of the ads related to that account has an ad with the column active set to true, then that should not be returned as well.
CREATE TABLE public.account
(
id bigint NOT NULL DEFAULT nextval('account_id_seq'::regclass),
password text COLLATE pg_catalog."default",
insert_time timestamp with time zone DEFAULT now(),
email_account_id bigint,
phone_number text COLLATE pg_catalog."default",
active boolean NOT NULL DEFAULT true,
CONSTRAINT account_pkey PRIMARY KEY (id),
CONSTRAINT account_email_account_id_fkey FOREIGN KEY (email_account_id)
REFERENCES public.email_account (id) MATCH SIMPLE
ON UPDATE CASCADE
ON DELETE CASCADE
)
CREATE TABLE public.ad
(
id bigint NOT NULL DEFAULT nextval('model_id_seq'::regclass),
name text COLLATE pg_catalog."default" NOT NULL,
insert_time timestamp with time zone DEFAULT now(),
ad_url text COLLATE pg_catalog."default",
account_id bigint NOT NULL,
active boolean NOT NULL DEFAULT true,
CONSTRAINT ad_pkey PRIMARY KEY (id),
CONSTRAINT ad_account_id_fkey FOREIGN KEY (account_id)
REFERENCES public.account (id) MATCH SIMPLE
ON UPDATE CASCADE
ON DELETE CASCADE
)
Below query will give result for only those account who does not have any ads with the column active set to true
SELECT * FROM public.account a
LEFT JOIN public.ad ad ON (a.id = ad.account_id)
WHERE a.active = true AND ad.account_id IS NULL;
using not exists():
select *
from public.account a
where not exists (
select 1
from public.ad d
where a.id = d.account_id
and d.active
)
select a.id
from public.account a
join public.ad ad on ad.account_id = a.id
group by a.id
having bool_or(ad.active) = false
If any related ad has active = true - bool_or(ad.active) will return true and the having condition will fail. So you will get accounts that have at least one related ad, and no of those ads is active.
Add more columns in the SELECT and GROUP BY clauses if needed.

Including a set of rows in a view column

Design:
A main table where each entry in it can have zero of more of a set of options “checked”. It seems to me that it would be easier to maintain (adding/removing options) if the options were part of a separate table and a mapping was made between the main table and an options table.
Goal:
A view that contains the information from the main table, as well as all options to which that row has been mapped. However the latter information exists in the view, it should be possible to extract the option’s ID and its description easily.
The implementation below is specific to PostgreSQL, but any paradigm that works across databases is of interest.
The select statement that does what I want is:
WITH tmp AS (
SELECT
tmap.MainID AS MainID,
array_agg(temp_options) AS options
FROM tstng.tmap
INNER JOIN (SELECT id, description FROM tstng.toptions ORDER BY description ASC) AS temp_options
ON tmap.OptionID = temp_options.id
GROUP BY tmap.MainID
)
SELECT tmain.id, tmain.contentcolumns, tmp.options
FROM tstng.tmain
INNER JOIN tmp
ON tmain.id = tmp.MainID;
However, attempting to create a view from this select statement generates an error:
column "options" has pseudo-type record[]
The solution that I’ve found is to cast the array of options (record[]) to a text array (text[][]); however, I’m interested in knowing if there is a better solution out there.
For reference, the create instruction:
CREATE OR REPLACE VIEW tstng.vsolution AS
WITH tmp AS (
SELECT
tmap.MainID AS MainID,
array_agg(temp_options) AS options
FROM tstng.tmap
INNER JOIN (SELECT id, description FROM tstng.toptions ORDER BY description ASC) AS temp_options
ON tmap.OptionID = temp_options.id
GROUP BY tmap.MainID
)
SELECT tmain.id, tmain.contentcolumns, CAST(tmp.options AS text[][])
FROM tstng.tmain
INNER JOIN tmp
ON tmain.id = tmp.MainID;
Finally, the DDL in case my description has been unclear:
CREATE TABLE tstng.tmap (
mainid INTEGER NOT NULL,
optionid INTEGER NOT NULL
);
CREATE TABLE tstng.toptions (
id INTEGER NOT NULL,
description text NOT NULL,
unwanted_column text
);
CREATE TABLE tstng.tmain (
id INTEGER NOT NULL,
contentcolumns text
);
ALTER TABLE tstng.tmain ADD CONSTRAINT main_pkey PRIMARY KEY (id);
ALTER TABLE tstng.toptions ADD CONSTRAINT toptions_pkey PRIMARY KEY (id);
ALTER TABLE tstng.tmap ADD CONSTRAINT tmap_pkey PRIMARY KEY (mainid, optionid);
ALTER TABLE tstng.tmap ADD CONSTRAINT tmap_optionid_fkey FOREIGN KEY (optionid)
REFERENCES tstng.toptions (id);
ALTER TABLE tstng.tmap ADD CONSTRAINT tmap_mainid_fkey FOREIGN KEY (mainid)
REFERENCES tstng.tmain (id);
You could create composite type e.g. temp_options_type with:
DROP TYPE IF EXISTS temp_options_type;
CREATE TYPE temp_options_type AS (id integer, description text);
After that just cast temp_options to that type within array_agg, so it returns temp_options_type[] instead of record[]:
DROP VIEW IF EXISTS tstng.vsolution;
CREATE OR REPLACE VIEW tstng.vsolution AS
WITH tmp AS
(
SELECT
tmap.MainID AS MainID,
array_agg(CAST(temp_options AS temp_options_type)) AS options
FROM
tstng.tmap INNER JOIN
(
SELECT id, description
FROM tstng.toptions
ORDER BY description
) temp_options
ON tmap.OptionID = temp_options.id
GROUP BY tmap.MainID
)
SELECT tmain.id, tmain.contentcolumns, tmp.options
FROM tstng.tmain
INNER JOIN tmp ON tmain.id = tmp.MainID;
Example result:
TABLE tstng.vsolution;
id | contentcolumns | options
----+----------------+-----------------------
1 | aaa | {"(1,xxx)","(2,yyy)"}
2 | bbb | {"(3,zzz)"}
3 | ccc | {"(1,xxx)"}
(3 rows)