SQL query for key value table with 1:n relation - sql

I have a table in which I want to store images. Each image has arbitrary properties that I want to store in a key-value table.
The table structure looks like this
id
fk_picture_id
key
value
1
1
camera
iphone
2
1
year
2001
3
1
country
Germany
4
2
camera
iphone
5
2
year
2020
6
2
country
United States
Now I want a query to find all pictures made by an iphone I could to something like this
select
fk_picture_id
from
my_table
where
key = 'camera'
and
value = 'iphone';
This works without any problems. But as soon as I want to add another key to my query I am get stucked. Lets say, I want all pictures made by an iPhone in the year 2020, I can not do something like
select
distinct(fk_picture_id)
from
my_table
where
(
key = 'camera'
and
value = 'iphone'
)
or
(
key = 'year'
and
value = '2020'
)
...because this selects the id 1, 4 and 5.
At the end I might have 20 - 30 different criteria to look for, so I don't think some sub-selects would work at the end.
I'm still in the design phase, which means I can still adjust the data model as well. But I can't think of any way to do this in a reasonable way - except to include the individual properties as columns in my main table.

A pattern you can consider here is to build a table of search parameters, then simply join this to your target table.
You would first create a temporary table with key and value columns then insert into it the search criteria values, any number of values you wish.
Using a CTE in place of a temporary table might look like:
with s as (
select 'camera' key, 'iphone' value
union all
select 'year', '2020'
)
select distinct t.fk_picture_id
from s
join t on t.key=s.key and t.value=s.value

The solution I found - thanks to this article
How to query data based on multiple 'tags' in SQL?
is that I made some changes to the database model
picture
id
name
1
Picture 1
2
Picture 2
And then I created a table for the tags
tag
id
tag
100
Germany
101
IPhone
102
United States
And the cross table
picture_tag
fk_picture_id
fk_tag_id
1
100
1
101
2
101
2
102
For a better understanding of the datasets
Picture
Tagname
Picture 1
Germany & Iphone
Picture 2
United States & IPhone
Now I can use the following statement
SELECT *
FROM picture
INNER JOIN (
SELECT fk_picture_id
FROM picture_tag
WHERE fk_tag_id IN (100, 101)
GROUP BY fk_picture_id
HAVING COUNT(fk_tag_id) = 2
) AS picture_tag
ON picture.id = picture_tag.fk_picture_id;
The only thing I need to do before the query is to collect the IDs of the tags I want to search for and put the number of tags in the having count statement.
If someone needs the example data, here are the sql statements for the tables and data
create table picture (
id integer,
name char(100)
);
create table tag (
id integer,
tag char(100)
);
create table picture_tag (
fk_picture_id integer,
fk_tag_id integer
);
insert into picture values (1, 'Picture 1');
insert into picture values (2, 'Picture 2');
insert into tag values (100, 'Germay');
insert into tag values (101, 'iphone');
insert into tag values (102, 'United States');
insert into picture_tag values (1, 100);
insert into picture_tag values (1, 101);
insert into picture_tag values (2, 101);
insert into picture_tag values (2, 102);

Related

How to search an entry in a table and return the column name or index in PostgreSQL

I have a table representing a card deck with 4 cards that each have a unique ID. Now i want to look for a specific card id in the table and find out which card in the deck it is.
card1
card 2
card3
card4
cardID1
cardID2
cardID3
cardID4
if my table would like this for example I would like to do something like :
SELECT column_name WHERE cardID3 IN (card1, card2, card3, card4)
looking for an answer i found this: SQL Server : return column names based on a record's value
but this doesn't seem to work for PostgreSQl
SQL Server's cross apply is the SQL standard cross join lateral.
SELECT Cname
FROM decks
CROSS join lateral (VALUES('card1',card1),
('card2',card2),
('card3',card3),
('card4',card4)) ca (cname, data)
WHERE data = 3
Demonstration.
However, the real problem is the design of your table. In general, if you have col1, col2, col3... you should instead be using a join table.
create table cards (
id serial primary key,
value text
);
create table decks (
id serial primary key
);
create table deck_cards (
deck_id integer not null references decks,
card_id integer not null references cards,
position integer not null check(position > 0),
-- Can't have the same card in a deck twice.
unique(deck_id, card_id),
-- Can't have two cards in the same position twice.
unique(deck_id, position)
);
insert into cards(id, value) values (1, 'KH'), (2, 'AH'), (3, '9H'), (4, 'QH');
insert into decks values (1), (2);
insert into deck_cards(deck_id, card_id, position) values
(1, 1, 1), (1, 3, 2),
(2, 1, 1), (2, 4, 2), (2, 2, 3);
We've made sure a deck can't have the same card, nor two cards in the same position.
-- Can't insert the same card.
insert into deck_cards(deck_id, card_id, position) values (1, 1, 3);
-- Can't insert the same position
insert into deck_cards(deck_id, card_id, position) values (2, 3, 3);
You can query a card's position directly.
select deck_id, position from deck_cards where card_id = 3
And there is no arbitrary limit on the number of cards in a deck, you can apply one with a trigger.
Demonstration.
This is a rather bad idea. Column names belong to the database structure, not to the data. So you can select IDs and names stored as data, but you should not have to select column names. And actually a user using your app should not be interested in column names; they can be rather technical.
It would probably be a good idea you changed the data model and stored card names along with the IDs, but I don't know how exactly you want to work with your data of course.
Anyway, if you want to stick with your current database design, you can still select those names, by including them in your query:
select
case when card1 = 123 then 'card1'
when card2 = 123 then 'card2'
when card3 = 123 then 'card3'
when card4 = 123 then 'card4'
end as card_column
from cardtable
where 123 in (card1, card2, card3, card4);

How to pass value as parameter to join tables in Oracle SQL

We have situation like each project has separate / unique table. and each table has unique column name.
For ex Project AAA is having table A1_table, in this table the column name will be A1_APP, A1_DOCUMENT, A1_Pages and so on.
Similarly for Project BBB will have table B1_Table and this table will have column name like B1_APP, B1_DOCUMENT, B1_Pages.
I am trying to join the table by passing the column name value as parameter. Since it will be difficult to change the column name for each project
Since we have different column name i could not able to join the table.
Kindly advise
Note :
The table is already created by vendor. i am just trying to extract data for all studies. so it will be difficult for me to rename the column one by one
Sql Script :
DECLARE
V_IMG_DOC_ID INT := '12345';
V_SHORT_DESC NVARCHAR2(100) := 'B18' ;
v_sql VARCHAR2(5000);
BEGIN
Select C.PROJECT "PROJECT", D.SUBJECT "SUBJECT_NO",D.SITE_NUMBER, E.IMAGE_ID,E.IMG_DOC_ID,F.
DTYPE_DESC "DOCUMENT_TYPE",E.IMG_FILENAME,E.IMG_NAT_FILE_ORG "FILE_LOCATION"
from APPLICATION A
inner join B18_DOCUMENT B on A. APP_ID = B.||V_SHORT_DESC||_APP_ID
inner join PROJECT_IMAGE E on E.IMG_DOC_ID = B.||V_SHORT_DESC||D_DOC_ID
inner join SUBJECT D on D.SJ_ID = B.||V_SHORT_DESC||D_SJ_ID
Inner join PROTOCOL C on C.APP_ID = A.APP_ID
inner join DOCUMENTTYPE F on F. DT_APP_ID = A. APP_ID and F. DT_ID =
B.||V_SHORT_DESC||D_DT_ID
where E.IMG_DOC_ID = 5877630
ORDER BY E.IMG_DOC_ID DESC;
END;
Refactor your code so that the projects are all in the same table and add a project_name column.
CREATE TABLE project_documents (
project_name VARCHAR2(10),
app_id NUMBER,
document CLOB,
pages VARCHAR2(50)
-- ,... etc.
)
If you want to restrict users to only seeing their own projects then you can use a virtual private database.
Then you do not need to use dynamic SQL to build queries with lots of different table names and can just use the one table for all projects and add a filter for the specific project using the added project_name column.
If you cannot do that then you are going to have to either:
use dynamic SQL to build the queries and dynamically set the table and column names each time you run the query; or
create a view of all the projects:
CREATE VIEW all_projects (project_name, app_id, document, pages /*, ... */) AS
SELECT 'A1', a1_app_id, a1_document, a1_pages /*, ... */ FROM a1_table UNION ALL
SELECT 'A2', a2_app_id, a2_document, a2_pages /*, ... */ FROM a2_table UNION ALL
SELECT 'B1', b1_app_id, b1_document, b1_pages /*, ... */ FROM b1_table UNION ALL
SELECT 'B18', b18_app_id, b18_document, b18_pages /*, ... */ FROM b18_table
and then you can query the view using the normalised column names rather than the project-specific names.
(Note: You will have to update the view when new projects are added.)
That looks like a terribly wrong data model. If you want to pass table/column names and use them in your queries, you'll have to use dynamic SQL which is difficult to maintain and debug.
By the way, do you really plan to duplicate, triplicate, ... all your tables to fit all new projects? That's insane!
Should be something like this (table_1 has a foreign key constraint, pointing to project table).
SQL> create table project
2 (id_project number constraint pk_proj primary key,
3 name varchar2(20) not null
4 );
Table created.
SQL> create table table_1
2 (id number constraint pk_t1 primary key,
3 id_project number constraint fk_t1_proj references project (id_project),
4 app varchar2(20),
5 document varchar2(20),
6 pages number
7 );
Table created.
Sample rows:
SQL> insert into project (id_project, name) values (1, 'Project AAA');
1 row created.
SQL> insert into project (id_project, name) values (2, 'Project BBB');
1 row created.
SQL> insert into table_1 (id, id_project, app) values (1, 1, 'App. 1');
1 row created.
SQL> insert into table_1 (id, id_project, app) values (2, 1, 'App. 2');
1 row created.
SQL> insert into table_1 (id, id_project, app) values (3, 2, 'App. 3');
1 row created.
Sample queries:
SQL> select * from project;
ID_PROJECT NAME
---------- --------------------
1 Project AAA
2 Project BBB
SQL> select a.id, p.name project_name, a.app
2 from table_1 a join project p on p.id_project = a.id_project
3 order by p.name, a.id;
ID PROJECT_NAME APP
---------- -------------------- --------------------
1 Project AAA App. 1
2 Project AAA App. 2
3 Project BBB App. 3
SQL>
I have created a view for this view and it worked fine as per my expectation

Join with json column?

I need find rows in table users by joining column in table queries.
I wrote some sql but it takes 0.200s to run, when SELECT * FROM ... takes 0.80s.
How can I improve performance?
db-fiddle example
The tables are :
CREATE TABLE users (
id INT,
browser varchar
);
CREATE TABLE queries (
id INT,
settings jsonb
);
INSERT INTO users (id,browser) VALUES (1, 'yandex');
INSERT INTO users (id, browser) VALUES (2, 'google');
INSERT INTO users (id, browser) VALUES (3, 'google');
INSERT INTO queries (id, settings) VALUES (1, '{"browser":["Yandex", "TestBrowser"]}');
and the query :
select x2.id as user_id, x1.id as query_id
FROM (
SELECT id, json_array_elements_text((settings->>'browser')::JSON) browser
FROM queries) x1
JOIN users x2 ON lower(x1.browser::varchar) = lower(x2.browser::varchar)
group by 1,2;
json_array_elements_text((settings->>'browser')::JSON)
'->>' converts the result to text. Then you cast it back to JSON. Doing that on one row (if you only have one) is not really going to be a problem, but it is rather pointless.
You could instead do:
jsonb_array_elements_text(settings->'browser')
ON lower(x1.browser::varchar) = lower(x2.browser::varchar)
You can create an index that can be used for this:
create index on users (lower(browser));
It won't do much good on a table with 3 rows. But presumably you don't really have 3 rows.

Updating Relational Tables using merge

In a hypothetical example, say I have two tables: FARM and FRUIT
FARM is organized like:
FARM_ID Size
1 50
2 100
3 200
...
and FRUIT is organized like:
Reference_ID FRUIT
1 Banana
1 Grape
1 Orange
2 Banana
2 Strawberry
FRUIT table is created from taking a parameter #fruit from excel which is a delimited string using '/'.
For example, #fruit = 'Banana/Grape/Orange'
And using a statement like:
INSERT INTO FRUIT(
Fruit,
Reference_ID,
)
SELECT Fruit, Scope_IDENTITY() from split_string(#fruit, '/')
Where split_string is a function.
My goal is to check for updates. I want to take in a Farm_ID and #fruit and check to see if any changes have been made to the fruit.
1) If the values haven't changed, dont do anything
2) If a new fruit was added, add it to the FRUIT table with the farm_ID
3) If there is a fruit in the FRUIT table that does not correspond to the new delimited list for the respectful FARM_ID, remove it from the FRUIT table.
I think a Merge statement would probably work but open to suggestions. Let me know if anything is unclear. Thank you
EDIT
Im fairly new to SQL but have tried using a merge...
Declare #foo tinyint
Merge Fruit as Target
Using (Select Fruit , #workingID From split_string(#fruit, '/') As source (fruit, ID)
--#workingID is just a way to get the ID from other parts of the sproc.
ON (TARGET.fruit = source.fruit)
WHEN MATCHED THEN
SET #foo = 1
WHEN NOT MATCHED
THEN DELETE
WHEN NOT MATCHED THEN
INSERT INTO FRUIT(
Reference_ID,
Fruit
)
VALUES(
Then I am a bit stuck on how to get unique, new values
Any way your input contains the new fruit list against the farm id. So better option is to delete the existing and insert the new list of fruit against the farmid.
Sample script is given below.
--loading the input to temp table
SELECT Fruit,#referenceid ReferenceId -- farmid corresponding tithe fruit list
INTO #temp
FROM Split_string(#fruit,'/')
-- delete the existing data against the given farmid
DELETE FROM fruit f
WHERE EXISTS ( SELECT 1 FROM #temp t
WHERE f.Reference_id=t.ReferenceId)
-- insert the new list
INSERT INTO fruit
SELECT fruit,referenceId
FROM #temp

Inserting multiple records in database table using PK from another table

I have DB2 table "organization" which holds organizations data including the following columns
organization_id (PK), name, description
Some organizations are deleted so lot of "organization_id" (i.e. rows) doesn't exist anymore so it is not continuous like 1,2,3,4,5... but more like 1, 2, 5, 7, 11,12,21....
Then there is another table "title" with some other data, and there is organization_id from organization table in it as FK.
Now there is some data which I have to insert for all organizations, some title it is going to be shown for all of them in web app.
In total there is approximately 3000 records to be added.
If I would do it one by one it would look like this:
INSERT INTO title
(
name,
organization_id,
datetime_added,
added_by,
special_fl,
title_type_id
)
VALUES
(
'This is new title',
XXXX,
CURRENT TIMESTAMP,
1,
1,
1
);
where XXXX represent "organization_id" which I should get from table "organization" so that insert do it only for existing organization_id.
So only "organization_id" is changing matching to "organization_id" from table "organization".
What would be best way to do it?
I checked several similar qustions but none of them seems to be equal to this?
SQL Server 2008 Insert with WHILE LOOP
While loop answer interates over continuous IDs, other answer also assumes that ID is autoincremented.
Same here:
How to use a SQL for loop to insert rows into database?
Not sure about this one (as question itself is not quite clear)
Inserting a multiple records in a table with while loop
Any advice on this? How should I do it?
If you seriously want a row for every organization record in Title with the exact same data something like this should work:
INSERT INTO title
(
name,
organization_id,
datetime_added,
added_by,
special_fl,
title_type_id
)
SELECT
'This is new title' as name,
o.organization_id,
CURRENT TIMESTAMP as datetime_added,
1 as added_by,
1 as special_fl,
1 as title_type_id
FROM
organizations o
;
you shouldn't need the column aliases in the select but I am including for readability and good measure.
https://www.ibm.com/support/knowledgecenter/ssw_i5_54/sqlp/rbafymultrow.htm
and for good measure in case you process errors out or whatever... you can also do something like this to only insert a record in title if that organization_id and title does not exist.
INSERT INTO title
(
name,
organization_id,
datetime_added,
added_by,
special_fl,
title_type_id
)
SELECT
'This is new title' as name,
o.organization_id,
CURRENT TIMESTAMP as datetime_added,
1 as added_by,
1 as special_fl,
1 as title_type_id
FROM
organizations o
LEFT JOIN Title t
ON o.organization_id = t.organization_id
AND t.name = 'This is new title'
WHERE
t.organization_id IS NULL
;