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

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.

Related

PostgreSQL can't aggregate data from many tables

For simplicity, I will write the minimum number of fields in the tables.
Suppose I have this tables: items, item_photos, items_characteristics.
create table items (
id bigserial primary key,
title jsonb not null,
);
create table item_photos (
id bigserial primary key,
path varchar(1000) not null,
item_id bigint references items (id) not null,
sort_order smallint not null,
unique (path, item_id)
);
create table items_characteristics (
item_id bigint references items (id),
characteristic_id bigint references characteristics (id),
characteristic_option_id bigint references characteristic_options (id),
numeric_value numeric(19, 2),
primary key (item_id, characteristic_id),
unique (item_id, characteristic_id, characteristic_option_id));
And I want to aggregate all the photos and characteristics of one item.
For a start, I got this.
select i.id as id,
i.title as title,
array_agg( ip.path) as photos,
array_agg(
array [ico.characteristic_id, ico.characteristic_option_id, ico.numeric_value]) as characteristics_array
FROM items i
LEFT JOIN item_photos ip on i.id = ip.item_id
LEFT JOIN items_characteristics ico on ico.item_id = i.id
GROUP BY i.id
The first problem here arises in the fact that if there are 4 entries in item_characteristics that relate to one item, and, for example, item_photos did not have entries, I get an array of four null elements in the photos field {null, null, null, null}.
So I had to use array_remove:
array_remove(array_agg(ip.path), null) as photos
Further, if I have 1 photo and 4 characteristics, I get a duplicate of 4 photo entries, for example: {img/test-img-1.png,img/test-img-1.png,img/test-img-1.png,img/test-img-1.png}
So I had to use distinct:
array_remove(array_agg(distinct ip.path), null) as photos,
array_agg(distinct
array [ico.characteristic_id, ico.characteristic_option_id, ico.numeric_value]) as characteristics_array
The decision is rather awkward as for me.
The situation is complicated by the fact that I had to add 2 more fields to item_characteristics:
string_value jsonb, --string value
json_value jsonb --custom value
And so I need to aggregate already 5 values ​​from item_characteristics, where 2 are already jsonb and distinct can have a very negative impact on performance.
Is there any more elegant solution?
Aggregate before joining:
SELECT i.id as id, i.title as title, ip.paths, null as photos,
ico.characteristics_array
FROM items i LEFT JOIN
(SELECT ip.item_id, array_agg( ip.path) as paths
FROM item_photos ip
GROUP BY ip.item_ID
) ip
ON ip.id = i.item_id LEFT JOIN
(SELECT ico.item_id,
array_agg(array [ico.characteristic_id, ico.characteristic_option_id, ico.numeric_value]
) as characteristics_array
FROM items_characteristics ico
GROUP BY ico.item_id
) ico
ON ico.item_id = i.id

SQL issue - type of fkey

I'm using PostgreSQL
What I need
In SELECT query I need to select owner_type (client or domain). If solution does not exist please help me to rework this schema.
Schema (tables)
Albums - id | client_id (fkey) | domain_id (fkey) | name
Clients - id | first_name | last_name
Domains - id | name
Description: Albums owner can be Client or Domain or future other Nodes...
1. CREATE TABLE QUERY
CREATE TABLE albums
(
id BIGSERIAL PRIMARY KEY,
client_id BIGINT,
domain_id BIGINT,
name VARCHAR(255) NOT NULL,
FOREIGN KEY (client_id) REFERENCES clients(id),
FOREIGN KEY (domain_id) REFERENCES domains(id),
CHECK ((client_id IS NULL) <> (domain_id IS NULL))
);
2. SELECT QUERY
SELECT albums.id,
albums.name,
COALESCE(c.id, d.id) AS owner_id
FROM albums
LEFT JOIN clients c
ON albums.client_id = c.id
LEFT JOIN domains d
ON albums.domain_id = d.id
Need something like -> if c.id === null -> owner_type = 'Domain'
You would seem to want:
SELECT a.id, a.name,
COALESCE(c.id, d.id) AS owner_id,
(CASE WHEN c.id IS NOT NULL THEN 'client' ELSE 'domain' END) as owner_type
FROM albums a LEFT JOIN
clients c
ON a.client_id = c.id LEFT JOIN
domains d
ON a.domain_id = d.id ;
Do you need two separate columns representing client_id and domain_id for the type of owners? It seems that if you were to add more nodes, you would have to add additional columns.
Could you have an owners table representing all types of owners, and have an owner_id foreign key on the albums table?
I was thinking something like this:
CREATE TABLE albums (
id BIGSERIAL PRIMARY KEY,
owner_id BIGINT,
name VARCHAR(255) NOT NULL,
FOREIGN KEY (owner_id) REFERENCES owners(id)
);
CREATE TABLE owners (
id BIGSERIAL PRIMARY KEY,
type VARCHAR(100) NOT NULL,
name VARCHAR(255) NOT NULL
);
You could then query for albums belonging to clients:
SELECT a.id, a.name, o.name AS owner_name
FROM albums a
JOIN owners o ON o.id = a.owner_id
WHERE o.type = 'Client';
As new nodes (types of owners) are added, you simply need to add them to the owners table without modifying the schema of the albums table.
Hope this helps.

Get rows that no foreign keys point to

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
)

SQL Ordering a complex query

Suppose I have these tables:
items, which stores items.
CREATE TABLE items
(
id serial NOT NULL,
name character varying(255),
rarity character varying(255),
created_at timestamp without time zone,
updated_at timestamp without time zone,
CONSTRAINT items_pkey PRIMARY KEY (id)
)
item_modifiers, which manages the many-to-many relationship between items and modifiers
CREATE TABLE item_modifiers
(
id serial NOT NULL,
item_id integer,
modifier_id integer,
primary_value integer,
secondary_value integer,
CONSTRAINT item_modifiers_pkey PRIMARY KEY (id)
)
modifiers , which contains all possible item modifiers
CREATE TABLE modifiers
(
id serial NOT NULL,
name character varying(255),
CONSTRAINT explicit_mods_pkey PRIMARY KEY (id)
)
Now suppose I have a complex query. I want to find all items that have modifiers with ID 1 and 2, ordered by the primary_value of the modifier with ID 1.
I have tried this query
SELECT "items".*, item_modifiers.primary_value FROM "items"
INNER JOIN item_modifiers ON item_modifiers.item_id = items.id
AND ((item_modifiers.modifier_id = 1)
OR (item_modifiers.modifier_id = 2))
GROUP BY items.id, item_modifiers.primary_value
HAVING
COUNT(item_modifiers.id) = 2
ORDER BY item_modifiers.primary_value DESC
But it returns an empty result set. However when I don't group by the primary_value, it does, but then I can't order it. I've been stuck on this for ages, so any help is greatly appreciated.
EDIT I have built an SQL fiddle to demonstrate
http://sqlfiddle.com/#!15/30887/1
You don't have to join with item_modifiers with the modifier_id 2, you only want it guaranteed that such a records EXISTS:
SELECT items.*, item_modifiers.primary_value
FROM items
INNER JOIN item_modifiers ON item_modifiers.item_id = items.id AND item_modifiers.modifier_id = 1
WHERE EXISTS
(
SELECT *
FROM item_modifiers
WHERE item_modifiers.item_id = items.id AND item_modifiers.modifier_id = 2
)
ORDER BY item_modifiers.primary_value DESC;
Here is your SQL fiddle: http://sqlfiddle.com/#!15/30887/9
Is this what you are looking for :
SELECT "items".*,
item_modifiers.primary_value
FROM "items"
INNER JOIN item_modifiers
ON item_modifiers.item_id = items.id
AND ( ( item_modifiers.modifier_id = 1 )
OR ( item_modifiers.modifier_id = 2 ) )
ORDER BY item_modifiers.primary_value DESC;

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)