How to create a subset query in sql? - sql

I have two tables as follows:
CREATE List (
id INTEGER,
type INTEGER REFERENCES Types(id),
data TEXT,
PRIMARY_KEY(id, type)
);
CREATE Types (
id INTEGER PRIMARY KEY,
name TEXT
);
Now I want to create a query that determines all ids of List which has given type strings.
For example,
List:
1 0 "Some text"
1 1 "Moar text"
2 0 "Foo"
3 1 "Bar"
3 2 "BarBaz"
4 0 "Baz"
4 1 "FooBar"
4 2 "FooBarBaz"
Types:
0 "Key1"
1 "Key2"
2 "Key3"
Given the input "Key1", "Key2", the query should return 1, 4.
Given the input "Key2", "Key3", the query should return 3, 4.
Given the input "Key2", the query should return 1, 3, 4.
Thanks!

select distinct l.id
from list l
inner join types t on t.id = l.type
where t.name in ('key1', 'key2')
group by l.id
having count(distinct t.id) = 2
You have to adjust the having clause to the number of keys you are putting in your where clause. Example for just one key:
select distinct l.id
from list l
inner join types t on t.id = l.type
where t.name in ('key2')
group by l.id
having count(distinct t.id) = 1
SQlFiddle example

You can use the following trick to extend Jurgen's idea:
with keys as (
select distinct t.id
from types t
where t.name in ('key1', 'key2')
)
select l.id
from list l join
keys k
on l.type = keys.id cross join
(select count(*) as keycnt from keys) k
group by l.id
having count(t.id) = max(k.keycnt)
That is, calculate the matching keys in a subquery, and then use this for the counts. This way, you only have to change one line to put in key values, and you can have as many keys as you would like. (Just as a note, I haven't tested this SQL so I apologize for any syntax errors.)

If you can dynamically produce the SQL, this may be one of the most efficent ways, in many DBMS:
SELECT l.id
FROM List l
JOIN Types t1 ON t1.id = l.type
JOIN Types t2 ON t2.id = l.type
WHERE t1.name = 'Key1'
AND t2.name = 'Key2' ;
See this similar question, with more than 10 ways to get the same result, plus some benchmarks (for Postgres): How to filter SQL results in a has-many-through relation

Related

How to group multiple columns into a single array or similar?

I would like my query to return a result structured like this, where tags is an array of arrays or similar:
id | name | tags
1 a [[1, "name1", "color1"], [2, "name2", color2"]]
2 b [[1, "name1", "color1"), (3, "name3", color3"]]
I expected this query to work, but it gives me an error:
SELECT i.id, i.name, array_agg(t.tag_ids, t.tag_names, t.tag_colors) as tags
FROM ITEMS
LEFT OUTER JOIN (
SELECT trm.target_record_id
, array_agg(tag_id) as tag_ids
, array_agg(t.tag_name) as tag_names
, array_agg(t.tag_color) as tag_colors
FROM tags_record_maps trm
INNER JOIN tags t on t.id = trm.tag_id
GROUP BY trm.target_record_id
) t on t.target_record_id = i.id;
Error:
PG::UndefinedFunction: ERROR: function array_agg(integer[], character varying[], character varying[]) does not exist
LINE 1: ..., action_c2, action_c3, action_name, action_desc, array_agg(...
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
This query works and produces similar results (but not quite what I want):
SELECT i.id, i.name, t.tag_ids, t.tag_names, t.tag_colors as tags as tags
FROM ITEMS
LEFT OUTER JOIN (
SELECT trm.target_record_id, array_agg(tag_id) as tag_ids, array_agg(t.tag_name) as tag_names, array_agg(t.tag_color) as tag_colors
FROM tags_record_maps trm
INNER JOIN tags t on t.id = trm.tag_id
GROUP BY trm.target_record_id
) t on t.target_record_id = i.id;
Result:
id | name | tag_ids | tag_names | tag_colors
1 a [1, 2] ["name1, "name2"] ["color1", "color2"]
1 a [1, 3] ["name1, "name3"] ["color1", "color3"]
Edit:
This query almost produces what I'm looking for, except it names the json keys f1, f2, f3. It would be perfect if I could name them id, name, color:
SELECT trm.target_record_id, json_agg( (t.id, t.tag_name, t.tag_color) )
FROM tags_record_maps trm
INNER JOIN tags t on t.site_id = trm.site_id and t.id = trm.tag_id
GROUP BY trm.target_record_id
having count(*) > 1;
Result:
[{"f1":1,"f2":"name1","f3":"color1"},{"f1":2,"f2":"name2","f3":"color2"}]
(t.id, t.tag_name, t.tag_color) is short syntax for ROW(t.id, t.tag_name, t.tag_color) - and a ROW constructor does not preserve nested attribute names. The manual:
By default, the value created by a ROW expression is of an anonymous record type. If necessary, it can be cast to a named composite type —
either the row type of a table, or a composite type created with
CREATE TYPE AS.
Bold emphasis mine. To also get proper key names in the result, cast to a registered composite type as advised in the quote, use a nested subselect, or simply use json_build_object() in Postgres 9.4 or newer (effectively avoiding the ROW constructor a priori):
SELECT trm.target_record_id
, json_agg(json_build_object('id', t.id
, 'tag_name', t.tag_name
, 'tag_color', t.tag_color)) AS tags
FROM tags_record_maps trm
JOIN tags t USING (site_id)
WHERE t.id = trm.tag_id
GROUP BY trm.target_record_id
HAVING count(*) > 1;
I use original column names, but you can chose your key names freely. In your case:
json_agg(json_build_object('id', t.id
, 'name', t.tag_name
, 'color', t.tag_color)) AS tags
Detailed explanation:
Return multiple columns of the same row as JSON array of objects
array_agg() puts one argument into an array. You could try to concatenate the values together:
array_agg(t.tag_ids || ':' || t.tag_names || ':' || t.tag_colors)
Or perhaps use a row constructor:
array_agg( (t.tag_ids, t.tag_names, t.tag_colors) )
Why not try a Json_Agg()?
SELECT
json_agg(tag_ids, tag_names, tag_colors)
FROM items
Etc...
DB fiddle
let's play with composite type.
create type tags as(tag_id bigint, tag_name text,tag_color text);
using array_agg:
select item_id,name, array_agg(row(trm.tag_id, tag_name, tag_color)::tags) as tags
from items i join tags_record_maps trm on i.item_id = trm.target_record_id
group by 1,2;
to json.
select item_id,name, to_json( array_agg(row(trm.tag_id, tag_name, tag_color)::tags)) as tags
from items i join tags_record_maps trm on i.item_id = trm.target_record_id
group by 1,2;
access individual/base element of composite type:
with a as(
select item_id,name, array_agg(row(trm.tag_id, tag_name, tag_color)::tags) as tags
from items i join tags_record_maps trm on i.item_id = trm.target_record_id
group by 1,2)
select a.item_id, a.tags[2].tag_id from a;

How to write a query to get data count with combination of codision

I have two tables named [DrugPrescriptionEdition] and [PrescriptionDoseDetail] and now, I join that two tables using the below query and taking a result set.
select * from DrugPrescription dp where id in(
SELECT distinct dpe.template
FROM [DrugPrescriptionEdition] dpe
join PrescriptionDoseDetail pdd on pdd.prescription = dpe.id
where doseEnd_endDate is NULL and doseEnd_doseEndType =1
)
but now I want to take records only contain, (1,2) combination of 'datasource' column and prescription.id should be same.
Example : like records { prescriptionID =4 and there contain ,(1,2) }. I will not consider, only 1 ,or 2 contain records.
Need some expert help to adding this conditions to my above query and modify it .
Expected result : I need to filter out , above query result using this, new condition too.
Let me assume your records are in a single table. Here is one method:
select t.*
from t
where (t.dataSource = 1 and
exists (select 1
from t t2
where t2. prescriptionid = t.prescriptionid and
t2.dataSource = 2
)
) or
(t.dataSource = 2 and
exists (select 1
from t t2
where t2.prescriptionid = t.prescriptionid and
t2.dataSource = 2
)
);
It is unclear if any other data sources are allowed. If they are not, then add:
and
not exists (select 1
from t t3
where t3.prescriptionid = t.prescriptionid and
t3.dataSource not in (1, 2)
)

How to find the key for the minimum value in jsonb column of postgres?

I need to find the key of the minimum value in a jsonb object,I have found out minimum value, need to find the key of the same in the same query.
Query I am using
SELECT id,min((arr ->> 2)::numeric) AS custom_value
FROM (
SELECT id, jdoc
FROM table,
jsonb_each(column1) d (key, jdoc)
) sub,
jsonb_each(jdoc) doc (key, arr)
group by 1
This will do the job.
The left join ... on 1=1 is for keeping IDs with empty json
select t.id
,j.key
,j.value
from mytable t
left join lateral (select j.key,j.value
from jsonb_each(column1) as j
order by j.value
limit 1
) j
on 1=1

Exclude results with join conditions

I'm trying to make an sql request with join exclusion.
Explains:
Table element
id # name #
1 Sea
2 tree
Table colour
id # name #
1 green
2 blue
3 brown
Table relation
element_id # colour_id
1 2
2 1
2 3
I have my working request for "get elements for one of these colours".
Exemple with green and blue:
SELECT element.name, colour.name FROM element
LEFT JOIN relation
ON (element.id = relation.element_id)
LEFT JOIN colour
ON (colour.id = relation.colour_id)
WHERE (relation.colour_id = 1 OR relation.colour_id = 2)
I would like make request for "get elements where they have a relation with all listed colors". Where for green and brown it returns tree.
I've tried to change the 'OR' to 'AND' but request return 0 results :/
General way to solve this problem is to filter values and count how many times they appear in result. If equal, all elements are found.
select element_id
from relation
where colour_id in (1, 2)
group by element_id
having count (distinct colour_id) = 2
Having this table one might join it to original tables to produce full column set:
SELECT element.name, colour.name
FROM relation
INNER JOIN
(
select element_id
from relation
where colour_id in (1, 2)
group by element_id
having count (distinct colour_id) = 2
) matches
ON relation.element_id = matches.element_id
INNER JOIN element
ON element.id = relation.element_id
INNER JOIN colour
ON colour.id = relation.colour_id
This type of query can be handled with some of SQL's set-based operators:
Which elements have relations for all colours?
Using the ALL operator (syntax may vary slightly by database):
SELECT element.name
FROM element
WHERE ( SELECT colour.id FROM relation
INNER JOIN colour ON colour.id = relation.colour_id
WHERE relation.element_id = element.id )
= ALL ( SELECT colour.id from colour)
;
Using the EXCEPT operator:
SELECT element.name
FROM element
WHERE NOT EXISTS
( SELECT colour.id from colour
EXCEPT
SELECT colour.id FROM relation
INNER JOIN colour ON colour.id = relation.colour_id
WHERE relation.element_id = element.id
)
;
There is a typo in your WHERE clause - one of the ids is 2 ("blue") instead of 3 ("brown"). It should be
WHERE (relation.colour_id = 1 OR relation.colour_id = 3)
(or in shorter form:
WHERE relation.colour_id IN (1, 3)
).
Note however, that your current query - although after this fix it should work for your sample data - won't give you correct results in general. It will give you the elements associated with any of the specified colors. The correct solution to this is given in #Nikola's answer though.
without subselects, I would suggest:
SELECT
e.id AS id
FROM
element AS e
LEFT OUTER JOIN relation AS r ON r.element_id = e.id
GROUP BY e.id HAVING SUM(CASE WHEN r.colour_id = 1 THEN 1 ELSE 0 END ) = 0
ORDER BY e.id ASC;
after this you can select the elements by id.

selecting latest rows per distinct foreign key value

excuse the title, i couldn't come up with something short and to the point...
I've got a table 'updates' with the three columns, text, typeid, created - text is a text field, typeid is a foreign key from a 'type' table and created is a timestamp. A user is entering an update and select the 'type' it corresponds too.
There's a corresponding 'type' table with columns 'id' and 'name'.
I'm trying to end up with a result set with as many rows as is in the 'type' table and the latest value from updates.text for the particular row in types. So if i've got 3 types, 3 rows would be returned, one row for each type and the most recent updates.text value for the type in question.
Any ideas?
thanks,
John.
select u.text, u.typeid, u.created, t.name
from (
select typeid, max(created) as MaxCreated
from updates
group by typeid
) mu
inner join updates u on mu.typeid = u.typeid and mu.MaxCreated = u.Created
left outer join type t on u.typeid = t.typeid
What are the actual columns you want returned?
SELECT t.*,
y.*
FROM TYPE t
JOIN (SELECT u.typeid,
MAX(u.created) 'max_created'
FROM UPDATES u
GROUP BY u.typeid) x ON x.typeid = t.id
JOIN UPDATES y ON y.typeid = x.typeid
AND y.created = x.max_created
SELECT
TYP.id,
TYP.name,
TXT.comment
FROM
dbo.Types TYP
INNER JOIN dbo.Type_Comments TXT ON
TXT.type_id = TYP.id
WHERE
NOT EXISTS
(
SELECT
*
FROM
dbo.Type_Comments TXT2
WHERE
TXT2.type_id = TYP.id AND
TXT2.created > TXT.created
)
Or:
SELECT
TYP.id,
TYP.name,
TXT.comment
FROM
dbo.Types TYP
INNER JOIN dbo.Type_Comments TXT ON
TXT.type_id = TYP.id
LEFT OUTER JOIN dbo.Type_Comments TXT2 ON
TXT2.type_id = TYP.id AND
TXT2.created > TXT.created
WHERE
TXT2.type_id IS NULL
In either case, if the created date can be identical between two rows with the same type_id then you would need to account for that.
I've also assumed at least one comment per type exists. If that's not the case then you would need to make a minor adjustment for that as well.