Transitive-Closure Table Restructure - mysql

How would I be able to retrieve a full-tree from the current structure, or refactor the current table structure to allow for an optimized recursive query?
Issue
Unable to retrieve full-tree of components from base component without iteration.
A single component can have an undefined number of connections (depth).
Components do not have a parent property, as each component can be related to multiple components.
Unable to recursively update affected attribute values of a component.
For example if the price of a component changes, the price is updated for all related components_of.
Current Structure
component
primary key (id)
| id | price |
|----|------ |
| A | 1 |
| B | 1 |
| C | 1 |
| D | 2 |
| E | 2 |
component_closure
unique index (component, component_of)
index (component_of)
FK (component) References component (id)
FK (component_of) References component (id)
| component | component_of |
|--------------------------|
| D | B |
| D | C |
| B | A |
| E | C |
| E | A |
Resulting Graph Model:
Example query:
UPDATE component
SET price = 2
WHERE id = 'A';
Desired Result (* indicates recursively updated values)
| id | price |
|----|------ |
| A | 2 |
| B | 2 | *
| C | 1 |
| D | 3 | *
| E | 3 | *
I am thinking I would need to store the entire tree relationship in the component_closure table, so that I would be able to retrieve the component_of relationships of all components and use a depth column to determine the order of components. Though that seems wasteful when the full-tree is not needed, such as for immediate components_of.
For example:
| component | component_of | depth |
|-----------|--------------|-------|
| D | A | 1 |
| D | B | 2 |
| D | C | 1 |

Yes, if you want to store the transitive closure, you need to store all paths.
For some operations, it's even helpful to store the path of length 0:
| component | component_of | depth |
|-----------|--------------|-------|
| D | D | 0 |
| D | A | 1 |
| D | B | 2 |
| C | C | 0 |
| B | B | 0 |
| B | A | 1 |
| A | A | 0 |
In MySQL 8.0, none of this will be needed. We'll finally be able to use recursive queries.

Related

How to make a pivot table by multiple unique ID numbers?

I'm trying to break up a SQL table that needs to take a users name and find the unique user ID's from up to 4 systems.
The data is currently like this:
| Name | User_ID |
-----------------
| A | 10 |
| A | 110 |
| A | 1500 |
| A | 4 |
| B | 20 |
| B | 100 |
| B | 2 |
| C | 10 |
I need to pivot it around the user's name to look like this (the id's don't need to be in numerical order as the SYS#_ID for each doesn't matter):
| Name | SYS1_ID | SYS2_ID | SYS3_ID | SYS4_ID |
------------------------------------------------
| A | 4 | 10 | 110 | 1500 |
| B | 2 | 20 | 100 | NULL |
| C | 10 | NULL | NULL | NULL |
This is the code I have tried on MySQL:
PIVOT(
COUNT(User_ID)
FOR Name
IN (SYS1_ID, SYS2_ID, SYS3_ID, SYS4_ID)
)
AS PivotedUsers
ORDER BY PivotedUsers.User_Name;
I'm unsure if PIVOT works on MySQL as I keep getting an error "PIVOT unknown". Is there a way to find the values that each user has and if they do not appear in the table already add them to the next column with a max of 4 values?

how to group selection from other table as one column in sql?

here is two tables:
a:
+-----+------------------------+
| id | conten |
+-----+------------------------+
| 1 | q. |
| 2 | q. |
| 3 | s. |
| 4 | g |
| 1 | a |
| 2 | a |
+-----+------------------------+
b:
+-----+------------------------+
| id | type |
+-----+------------------------+
| 1 | I. |
| 2 | II. |
| 3 | III. |
| 4 | IV |
| 5 | V |
| 6 | VI |
+-----+------------------------+
Is there a way to select from a and b so that for one id 2, there will be one additional field that groups all content from that id? the select result should be something like this:
+-----+------------------------+-----------+
| id | type | contents |
+-----+------------------------+-----------+
| 2 |I. | q,a |
+-----+------------------------+-----------+
Edited
btw, if there is a way to do it by sqlahcmey, that would be sweet.
SELECT b.id, b.type, IFNULL(GROUP_CONCAT(a.conten), '') AS contents
FROM b
LEFT JOIN a ON a.id = b.id
GROUP BY b.id
See How do I write a group_concat function in sqlalchemy? for how to translate GROUP_CONCAT to SQLAlchemy.

Get average from nested eloquent relationship

I would like to calculate average from nested relationship between eloquent models. So, let's say, I have 3 tables called programs, activities and statistics.
For simplicity sake, I will try to minimize the structure as follows:
program table:
-------------
| id | name |
-------------
| 1 | Foo |
| 2 | Bar |
-------------
activities table:
-----------------------------------
| id | program_id | name |
-----------------------------------
| 1 | 1 | Foo 1 |
| 2 | 1 | Foo 2 |
| 3 | 1 | Foo 3 |
| 4 | 2 | Bar 1 |
| 5 | 2 | Bar 2 |
-----------------------------------
statistics table:
-----------------------------------
| id | activity_id | type | score |
-----------------------------------
| 1 | 1 | A | 25 |
| 2 | 1 | B | 20 |
| 3 | 1 | A | 22 |
| 4 | 2 | A | 27 |
| 5 | 2 | B | 24 |
| 6 | 3 | A | 23 |
-----------------------------------
Now, what I want to get is the average of score of a program with specific type of statistic. I defined relationship in models, and tried following code, but no avail:
$program = Program::find(1);
$avg = $program->activities->where('statistics.type', 'A')->avg('statistics.value');
$avg always 0 or null if there is no activities in program, even without where clause.
i'm sure that i defined the relationship correctly because $program->activities returns a sets of activities and $activity-> statistics return a sets of statistics as well.
Any ideas?
You can use whereHas() like this:
Statistics::whereHas('activity', function ($q) use($programId) {
$q->where('program_id', $programId);
})
->where('type', 'A')
->avg('score');
Make sure you've defined activity relationship which should be "statistics belongsTo() activity".

Migrate data from a link table into target table

I have two tables. One (let's call it a) is currently a link table with data in like this
| c_id | t_id |
|-------|-------|
| 1 | 8 |
| 1 | 9 |
| 2 | 8 |
| 3 | 8 |
| 4 | 9 |
and another (t) with data like this
| id | code | value |
|-------|-------|-------|
| 1 | AB | 0.9 |
| 2 | BC | 0 |
| 3 | IM | 0 |
| 4 | MC | 0 |
| 5 | VI | 0 |
| 6 | BC | 0.9 |
| 7 | MC | 2.5 |
| 8 | VI | 2.5 |
| 9 | BC | 2.5 |
t_id in table a is a foreign key mapping onto id in table t, which is an auto incremented ID.
Due to functionality changes, I now want the data from a to replicate the linked row in t and add the required c_id (and then table a to be dropped) so you get something like this;
| id | c_id | code | value |
|-------|-------|-------|-------|
...
| 25 | 1 | VI | 2.5 |
| 26 | 2 | VI | 2.5 |
| 27 | 3 | VI | 2.5 |
| 28 | 1 | BC | 2.5 |
| 29 | 4 | BC | 2.5 |
which will enable me to change the value column per c_id, rather than globally. The new rows can safely be added to the end of the table - or perhaps it would be better to have a new table with this information in.
Is there a query that can do this? I hope I don't have to do it by hand!
Assuming I'm understanding correctly, since you mentioned modifying tables, this is a one-time procedure.
You won't be able to add the rows to the end of either of the existing tables, since you have different column requirements. You'll have to either make a new table or modify the existing one. I chose the former, and then you can populate it using CREATE TABLE ... SELECT ... syntax:
CREATE TABLE new_t (id SERIAL, c_id INT, code VARCHAR(2), value FLOAT);
INSERT INTO new_t (c_id, code, value)
SELECT a.c_id, t.code, t.value FROM a INNER JOIN t ON (t.id = a.t_id);
http://sqlfiddle.com/#!9/c6765/2

Compare different rows and bring out result

I have a table which requires me to pair certain rows together using a unique value that both the rows share.
For instance in the below table;
+--------+----------+-----------+-----------+----------------+-------------+
| id | type | member | code | description | matching |
+--------+----------+-----------+-----------+----------------+-------------+
| 1000 |transfer | 552123 | SC120314 | From Gold | |
| 1001 |transfer | 552123 | SC120314 | To Platinum | |
| 1002 |transfer | 833612 | SC120314 | From silver | |
| 1003 |transfer | 833612 | SC120314 | To basic | |
| 1004 |transfer | 457114 | SC150314 | From Platinum | |
| 1005 |transfer | 457114 | SC150314 | To silver | |
| 1006 |transfer | 933276 | SC180314 | From Gold | |
| 1007 |transfer | 933276 | SC180314 | From To basic | |
+--------+----------+-----------+-----------+----------------+-------------+
basically What i need the query / routine to do is find the rows where the value in the 'member' column for each row match. Then see if the values in the 'code' column for the same found rows also match.
If both columns for both rows match, then assign a value to the 'matching' column for both rows. This value should be the same for both rows and unique to only them.
The unique code can be absolutely anything, so long as it's exclusive to matching rows. Is there any query / routine capable of carrying this out?
I'm not sure I understand the question correctly, but if you like to pick out and update rows where the code and member columns matches and set matching to some unique value for each of the related rows, I believe this would work:
UPDATE <table> A
INNER JOIN (SELECT * FROM <table>) B ON
B.member = A.member && B.code = A.code && A.id <> B.id
SET A.matching = (A.id + B.id);
The matching value will be set to the sum of the id columns for both rows. Notice that updating the matching field this way will not work if there are more than two rows that can match.
Running the above query against your example table would yield:
+------+----------+--------+----------+---------------+----------+
| id | type | member | code | description | matching |
+------+----------+--------+----------+---------------+----------+
| 1000 | transfer | 552123 | SC120314 | From Gold | 2001 |
| 1001 | transfer | 552123 | SC120314 | To Platinum | 2001 |
| 1002 | transfer | 833612 | SC120314 | From Silver | 2005 |
| 1003 | transfer | 833612 | SC120314 | To basic | 2005 |
| 1004 | transfer | 457114 | SC150314 | From Platinum | 2009 |
| 1005 | transfer | 457114 | SC150314 | To silver | 2009 |
| 1006 | transfer | 933276 | SC180314 | From Gold | 2013 |
| 1007 | transfer | 933276 | SC180314 | From To basic | 2013 |
+------+----------+--------+----------+---------------+----------+
I can give you a simple query what can do what you need.
tst is the name of the table.
SELECT *, COUNT( t2.id ) as matching FROM tst t LEFT JOIN tst t2 ON t2.member = t.member GROUP BY t.id