0

I searched a lot but found nothing.

My scenario is:

I have database with two tables table_item and table_item_linked. table_item has many items. User will come and add item(s). Later other user come and link one item with other item(s) via a form with two dropdown.

What I did so far is:

Structure of table_item:

+-------------------+
| table_item        |
+-------------------+
| item_id (Primary) |
| others            |
| ....              |
| ....              |
| ....              |
+-------------------+

Structure of table_item_linked:

+---------------------+
| table_item_linked   |
+---------------------+
| linked_id           | (Primary)
| item_id             | (Foreign key referencing -> item_id of table_item) 
| linked_items        | (here I need to store ids of linked items)    
| linked_by           | (referencing to user_id of user_table)           
| linked_timestamp    | (timestamp) 
+---------------------+

If I have items in table_item like: A B C D E F G H

When I link D with G

I can successfully fetch G when I am fetching D or vice versa. But problem came when I

Link H with G

So I must fetch D H while fetching G.

(D H G are linked in all means and upon fetching one, the remaining two must be attached and fetched)

It is like a multiple relation (Many to Many relationship).

Guys I know there must be professional way to do it. I will like to have any guidance. I can even change my database structure.

PS: Please don't suggest to add #tag as one item is exactly similar to the other linked.

UPDATES

Frontend looks like this. If I intend to link two records I will have two dropdowns as shown: Two dropdowns for linking records with one another

And If I check details of record A Record A Details

And If I check details of record B Record B Details

And If I check details of record C Record C Details

fWd82
  • 840
  • 1
  • 13
  • 31
  • i dont even understand what exactly you want - if i link `H` with `G` than usually you don't have a link from `G` to `H` unless you want them - but what caught my eyes first was your column `linked_items`- never ever store relations in a comma separated way - just normalize your data - i suggest you provide some example data what exactly your problem is because right now i can't figure out your problem... – Atural Jun 24 '19 at 12:52
  • I am sorry I am not so clear. Think of a real scenario where we have one 'word' of one language and I want to link it to another word of another language _(like translation)_ so i am viewing one word, I must have words from other languages that are attached to it as well. Yes, I want to link `H` with `G` and the `G` or `H` to any other item as well. I am open for suggestions if it cost me change my 'noob-ish' database design. – fWd82 Jun 24 '19 at 12:59
  • "Parent:child" is easy to implement inside a singe table. But that is a 1:many relationship with no loops. Many:many requires an extra table. – Rick James Jun 24 '19 at 21:45
  • Please use English and foreign words instead of G and H. By carefully picking examples, an English speaker does not need to know the other language. Example: mouse:Maus and car:auto for English:German. – Rick James Jun 24 '19 at 21:53
  • Japanese has a dozen ways to translate "no" -- depending on how polite you need to be. Does this mean that you want "no" to be 'linked' to 12 Japenese words? – Rick James Jun 24 '19 at 21:54
  • @RickJames, I am working on volunteer project, where anyone can add/edit Proverbs, and also link one Proverb of one language to other proverb in other language. You can check [here](https://proverbona.fawadiqbal.com/), Proverb can be linked [here](https://proverbona.fawadiqbal.com/proverb/link_two_proverbs), I will be deleting this comment once you read it. – fWd82 Jun 26 '19 at 19:14
  • @fWd82 - Ok, I have seen it. Seems like we chatted before. – Rick James Jun 27 '19 at 03:11
  • Read up on "transitive closure". Once you understand transitive closure, you will be able to reframe your problem in a way that is much easier to work with. – Walter Mitty Aug 10 '19 at 12:06

2 Answers2

1

The obvious solution is to store one row for each link in table_item_linked.

Your table then becomes

+---------------------+
| table_item_linked   |
+---------------------+
| linked_id           | (Primary
| from_item_id        | (The item linked _from_ -> item_id of table_item) 
| to_item_id          | the item linked _to_  
| linked_by           | (referencing to user_id of user_table)           
| linked_timestamp    | (timestamp) 
+---------------------+

In your example, the data would be:

linked_id     from_item_id    to_item_id   linked_by   linked_timestamp
------------------------------------------------------------------------
1                        D            H            sd      '1 jan 2020'
2                        H            G            sa      '2 Jan 2020'

You then need to write a hierarchical query to retrieve all the "children" of G.

Neville Kuyt
  • 29,247
  • 1
  • 37
  • 52
  • Thank You Sir, I am working on volunteer project, where anyone can add/edit Proverbs, and also link one Proverb of one language to other proverb in other language. You can check [here](https://proverbona.fawadiqbal.com/), Proverb can be linked [here](https://proverbona.fawadiqbal.com/proverb/link_two_proverbs), I will be deleting this comment once you read it. PS: I have modified my table as per your instructions, and trying hard to write query for it. Will get back soon. – fWd82 Jun 26 '19 at 19:18
  • Dear Sir @Neville Kuyt Thank you very much. Your answer did open a lot of windows for me to learn its been long I am still learning but I am unable to wrote this query. Can you please help me in writing above hierarchical query. I can't even sleep. Please Sir, edit you answer and include query for retrieving all childrens of `G` Thank You. – fWd82 Aug 08 '19 at 21:33
1

Assuming your table_item looks like this:

create table table_item (
  item_id int unsigned auto_increment not null,
  record  varchar(50),
  primary key (item_id)
);

insert into table_item (record) values
  ('Record A'),
  ('Record B'),
  ('Record C'),
  ('Record D'),
  ('Record E'),
  ('Record F'),
  ('Record G'),
  ('Record H');

table_item_linked could then be

create table table_item_linked (
  linked_id int unsigned auto_increment not null,
  item1_id  int unsigned not null,
  item2_id  int unsigned not null,
  linked_by int unsigned not null,
  linked_timestamp timestamp not null default now(),
  primary key (linked_id),
  unique key  (item1_id, item2_id),
  index       (item2_id, item1_id),
  foreign key (item1_id) references table_item(item_id),
  foreign key (item2_id) references table_item(item_id)
);

This is basically a many-to-many relation between items of the same type.

Note that you usually don't need an AUTO_INCREMENT column here. You can remove it, and define (item1_id, item2_id) as PRIMARY KEY. And linked_by should be a FOREGN KEY referencing the users table.

If a user (with ID 123) wants to link "Record A" (item_id = 1) with "Record B" (item_id = 2) and "Record B" (item_id = 2) with "Record C" (item_id = 3), your INSERT statements would be:

insert into table_item_linked (item1_id, item2_id, linked_by) values (1, 2, 123);
insert into table_item_linked (item1_id, item2_id, linked_by) values (2, 3, 123);

Now - When the user selects "Record A" (item_id = 1), you can get all related items with a recursive query (Requires at least MySQL 8.0 or MariaDB 10.2):

set @input_item_id = 1;

with recursive input as (
  select @input_item_id as item_id
), rcte as (
  select item_id from input

  union distinct

  select t.item2_id as item_id
  from rcte r
  join table_item_linked t on t.item1_id = r.item_id

  union distinct

  select t.item1_id as item_id
  from rcte r
  join table_item_linked t on t.item2_id = r.item_id
)
  select i.*
  from rcte r
  join table_item i on i.item_id = r.item_id
  where r.item_id <> (select item_id from input)

The result will be:

item_id    record
———————————————————
      2    Record B
      3    Record C

db-fiddle

In your application you would remove set @input_item_id = 1; and change select @input_item_id as item_id using a placeholder to select ? as item_id. Then prepare the statement and bind item_id as parameter.

Update

If the server doesn't support recursive CTEs, you should consider to store redundat data in a separate table, which is simple to query. A closure table would be an option, but it's not necessery here, and might consume too much storage space. I would group items that are connected together (directly and indirectly) into clusters.

Given the same schema as above, we define a new table table_item_cluster:

create table table_item_cluster (
  item_id    int unsigned not null,
  cluster_id int unsigned not null,
  primary key (item_id),
  index       (cluster_id, item_id),
  foreign key (item_id) references table_item(item_id)
);

This table links items (item_id) to clusters (cluster_id). Since an item can belong only to one cluster, we can define item_id as primary key. It's also a foreign key referencing table_item.

When a new item is created, it's not connected to any other item and builds an own cluster. So when we insert a new item, we need also to insert a new row in table_item_cluster. For simplicity we identify the cluster by item_id (item_id = cluster_id). This can be done in the application code, or with the following trigger:

delimiter //
create trigger table_item_after_insert 
  after insert on table_item
  for each row begin
    -- create a new cluster for the new item
    insert into table_item_cluster (item_id, cluster_id)
      values (new.item_id, new.item_id);
  end//
delimiter ;

When we link two items, we simply merge their clusters. The cluster_id for all items from the two merged clusters needs to be the same now. Here I would just take the least one of two. Again - we can do that in application code or with a trigger:

delimiter //
create trigger table_item_linked_after_insert 
  after insert on table_item_linked
  for each row begin
    declare cluster1_id, cluster2_id int unsigned;

    set cluster1_id = (
      select c.cluster_id
      from table_item_cluster c
      where c.item_id = new.item1_id
    );

    set cluster2_id = (
      select c.cluster_id
      from table_item_cluster c
      where c.item_id = new.item2_id
    );

    -- merge the linked clusters
    update table_item_cluster c
    set c.cluster_id = least(cluster1_id, cluster2_id)
    where c.item_id in (cluster1_id, cluster2_id);
  end//
delimiter ;

Now - When we have an item and want to get all (directly and indirectly) linked items, we just select all items (except of the given item) from the same cluster:

select i.*
from table_item i
join table_item_cluster c on c.item_id = i.item_id
join table_item_cluster c1
  on  c1.cluster_id = c.cluster_id
  and c1.item_id <> c.item_id -- exclude the given item
where c1.item_id = ?

db-fiddle

The result for c1.item_id = 1 ("Record A") would be:

item_id    record
———————————————————
      2    Record B
      3    Record C

But: As almost always when dealing with redundant data - Keeping it in sync with the source data can get quite complex. While it is simple to add and merge clusters - When you need to remove/delete an item or a link, you might need to split a cluster, which may require writing recursive or iterative code to determine which items belong to the same cluster. Though a simple (and "stupid") algorithm would be to just remove and reinsert all affected items and links, and let the insert triggers do theit work.

Update 2

Last but not least: You can write a stored procedure, which will iterate through the links:

delimiter //
create procedure get_linked_items(in in_item_id int unsigned)
begin
  set @ids := concat(in_item_id);
  set @ids_next := @ids;
  set @sql_tpl := "
    select group_concat(distinct id order by id) into @ids_next
    from (
      select item2_id as id
      from table_item_linked
      where item1_id in ({params_in})
        and item2_id not in ({params_not_in})
      union all
      select item1_id
      from table_item_linked
      where item2_id in ({params_in})
        and item1_id not in ({params_not_in})
    ) x
  ";

  while (@ids_next is not null) do
    set @sql := @sql_tpl;
    set @sql := replace(@sql, '{params_in}', @ids_next);
    set @sql := replace(@sql, '{params_not_in}', @ids);
    prepare stmt from @sql;
    execute stmt;
    set @ids := concat_ws(',', @ids, @ids_next);
  end while;

  set @sql := "
    select *
    from table_item
    where item_id in ({params})
      and item_id <> {in_item_id}
  ";
  set @sql := replace(@sql, '{params}', @ids);
  set @sql := replace(@sql, '{in_item_id}', in_item_id);

  prepare stmt from @sql;
  execute stmt;
end//
delimiter ;

To get all linked items of "Record A" (item_id = 1), you would use

call get_linked_items(1);

db-fiddle

To explain it in pseudo code:

  1. Initialize @ids and @ids_next with the input parameter
  2. Find all item IDs which are directly linked to any ID in @ids_next except of those, which are already in @ids
  3. Store the result into @ids_next (overwrite it)
  4. Append IDs from @ids_next to @ids (merge the two sets into @ids)
  5. If @ids_next is not empty: GOTO step 2.
  6. Return all items with IDs in @ids
Paul Spiegel
  • 30,925
  • 5
  • 44
  • 53
  • Hi, Thank You. I tried creating `table_item_linked` with the script you given and it's giving me error: `MySQL said: [Ref1] #1005 - Can't create table mydb.table_item_linked (errno: 150 "Foreign key constraint is incorrectly formed") ([Details…](http://localhost/phpmyadmin/server_engines.php?engine=InnoDB&page=Status) )` Seems like I am using `XAMPP Control Panel v3.2.3` with `Version: '10.1.39-MariaDB'` problem is I dont have v10.2+ on my Cpanel :/ [Ref1](http://localhost/phpmyadmin/url.php?url=https%3A%2F%2Fdev.mysql.com%2Fdoc%2Frefman%2F5.7%2Fen%2Ferror-messages-server.html) – fWd82 Aug 10 '19 at 21:13
  • 1
    @fWd82 The DATA TYPE of `item1_id` and `item2_id` must be exactly the same as for `item_id` in `table_item`. Since you didn't post the schema, I can't know which it is. – Paul Spiegel Aug 13 '19 at 18:00
  • Thank You. I fix that error by adding `unsigned` property to one my `id`. Well now when I was working on UPDATE 2 option. I am facing problem in creating trigger it says: `Unrecognized statement type (near end)` when I am executing query directly from SQL tab. However when I go to GUI Triggers. I am facing this: `MySQL said: #1064 - You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '// create trigger table_item_after_insert after insert on table_item for' at line 1` – fWd82 Aug 14 '19 at 18:42
  • 1
    Which GUI are you using? When writing a trigger with a GUI you brobably only need the code from `BEGIN` to `END`. – Paul Spiegel Aug 14 '19 at 18:51
  • `phpMyAdmin: Version information: 4.8.5`. I read [somewhere](https://stackoverflow.com/a/20055128/5737774) that I just need to paste query which is inside `delimiter //` by removing this, I am getting this error: `One or more errors have occurred while processing your request:` .. `MySQL said: #1303 - Can't create a TRIGGER from within another stored routine`, I have no triggers at all. – fWd82 Aug 14 '19 at 18:54
  • 1
    For **UPDATE 2** you should create a STORED PROCEDURE not a TRIGGER. – Paul Spiegel Aug 14 '19 at 18:57
  • Oh I am so sorry. I mean **UPDATE** (Update 1). This one: `delimiter // create trigger table_item_after_insert .. after insert on table_item ... end// delimiter ;` I am so ignorant. – fWd82 Aug 14 '19 at 19:03
  • 1
    Please read the [answer](https://stackoverflow.com/questions/20054814/cant-create-a-trigger-from-within-another-stored-routine-what-is-another-st/20055128#20055128) that you have linked. Remove anything before `begin` and after `end`. Or use this [answer](https://stackoverflow.com/a/2847741/5563083). – Paul Spiegel Aug 14 '19 at 19:06
  • Wow. It's working. I have tried UPDATE (update 1). The correct trigger was: `BEGIN insert into table_item_cluster (item_id, cluster_id) values (new.item_id, new.item_id); END` because everything else was defined in GUI in it's own textboxes. After successful triggers. I tried fetching but got no results. So I had to delete/truncate all data from tables and insert it again. Sir you are great. I wish I could be of some help. :( – fWd82 Aug 14 '19 at 19:41
  • Sir @Paul Spiegel I will love to try the other methods as well. I email my server administrator to upgrade MySQL or Maria DB versions. But Sir, I will also love to try query for `Neville Kuyt` [answer](https://stackoverflow.com/a/56737136/5737774). I someone could write a sample query so everyone would get benefit from. – fWd82 Aug 14 '19 at 19:47