SQL: Query adjacent nodes in a directed graph - mysql

I have a graph with nodes {A, B, C, D, ...} and a table which specifies the directed edges between them.
| node_1 | node_2 |
|-----------------|
| A | B |
| A | C |
| B | A |
| B | D |
| D | A |
We write A ~ B if there is an edge from A to B. So a row where node_1 = A and node_2 = B implies A ~ B. I distinguish between the following types of relations:
A = B if A ~ B and B ~ A
A > B if A ~ B and not B ~ A
A < B if B ~ A and not A ~ B
How can I retrieve all the nodes adjacent to a given node along with their type of relation? For example, a query for A on the above table should return
| node | type |
|------|------|
| B | = | (because A ~ B and B ~ A)
| C | > | (because A ~ C and not C ~ A)
| D | < | (because D ~ A and not A ~ D)

here is one way :
select node,
case when count(*) = 2 then '='
when max(ordertype) = 1 then '>'
when max(ordertype) = 2 then '<' end as type
from (select node2 node,
1 ordertype
from nodes
where node1 = 'A'
union all
select node1,
2
from nodes
where node2 = 'A') t
group by node
order by node

Hmmm . . . you can use conditional logic with aggregation:
select (case when node_1 = 'A' then node_2 else node_1 end) as other_node,
(case when count(*) = 2 then '='
when max(node_1) = 'A' then '>'
else '<'
end) as type
from nodes n
where 'A' in (node_1, node_2)
group by (case when node_1 = 'A' then node_2 else node_1 end);
Here is a db<>fiddle.
This seems like the simplest and probably the most performant solution.

Related

How to get column that have the same value

Sorry for my lack of knowledge in MySQL but how do you get the same value in different field?
For example if i have a table like so :
+-----+-----+-----+
| id | A | B |
+=====+=====+=====+
| 1 | 1 | 2 |
| 2 | 3 | 1 |
+-----+-----+-----+
And i would like to get the value 1 from A and B.
I tried doing something like :
SELECT A, B FROM table_name WHERE A = 1 AND B = 1
But this won't return a value if somehow A or B doesn't have the value 1. In cases like that I want it to only return the value 1 from the column that does have it. I want something like this :
SELECT A, B FROM table_name WHERE value = 1
I think you want OR:
SELECT A, B
FROM t
WHERE A = 1 OR B = 1
This can be shortened using IN:
WHERE 1 IN (A, B)
You should use query as:
SELECT A, B FROM table_name WHERE A = 1 OR B = 1
Here, you get rows which have either A=1 or B=1 or both A & B = 1.

sql query to compare values row wise?

i have a table which contains some data as an example:
+----------+-----+------+
| order_id | poi | povi |
+----------+-----+------+
| 1 | A | a |
| 1 | B | b |
| 1 | C | c |
| 2 | A | a |
| 2 | B | b |
| 2 | C | c |
| 3 | A | a |
| 3 | B | b |
| 4 | C | c |
| 5 | A | a |
| 5 | B | b |
| 6 | C | c |
| 7 | A | a |
| 8 | B | b |
| 9 | C | c |
+----------+-----+------+
i have 3 set of values of poi and povi like {A,a},{B,b},{C,c}
i want to get the order_id which contains all three of them, like in the above case the output should be.(order_id which have poi and povi as {A,a} and {B,b} and {C,c} but the problem is that they are diffrent rows)
+----------+
| order_id |
+----------+
| 1 |
| 2 |
+----------+
any idea?
So many times people just getting started ask similar questions to those already asked and answered, including this common scenario. However, not being able to apply know answers to your scenario doesn't help you wrap your head around what is asked, or how the query works in their own scenario... That said, lets look at yours.
You want all DISTINCT orders that have ALL of the following A/a, B/b, C/c entries. Multiple ways to resolve, but the most common is with a where / group by / having.
Start with something simple, looking for any order that has A/a
select
yt.Order_id
from
YourTable yt
where
( yt.poi = 'A' AND yt.poiv = 'a' )
and you would get order 1, 2, 3, 5 and 7. That is simple...
Now, add in your other criteria
select
yt.Order_id
from
YourTable yt
where
( yt.poi = 'A' AND yt.poiv = 'a' )
OR ( yt.poi = 'B' AND yt.poiv = 'b' )
OR ( yt.poi = 'C' AND yt.poiv = 'c' )
and this will give you all rows, but not what you want, but you should be able to see the where criteria is checking for both parts of POI / POIV with an OR between each possible combination. You obviously can not have one record that has a POI of both "A" and "B", that is why the "OR" between each paired ( AND ) criteria. But again, this gives ALL rows. But it is also qualifying only the pieces. So lets add one next step... a group by via the order, but HAVING clause expecting 3 records...
select
ytA.Order_id
from
YourTable ytA
where
( yt.poi = 'A' AND yt.poiv = 'a' )
OR ( yt.poi = 'B' AND yt.poiv = 'b' )
OR ( yt.poi = 'C' AND yt.poiv = 'c' )
group by
yt.Order_id
HAVING
count(*) = 3
The count(*) is to count how many records qualified the WHERE clause and will only return those records that had 3 entries.
Now, what if someone has multiple orders of A/a, A/a, B/b... This COULD give a false answer returned value, but please confirm these queries to meet your needs.
Although accepted, here is another way I would have written the query... somewhat similar to another post below. The premise of this version of the query is to utilize an index and qualify at least 1 record found before trying to find ALL. In this case, it first qualifies for those with an A/a. If an order does not have that, it does not care about looking for a B/b, C/c. If it DOES, then the join qualifies to the next levels too
select
ytA.Order_id
from
YourTable ytA
JOIN YourTable ytB
on ytA.Order_id = ytB.Order_id
AND ytB.poi = 'B'
AND ytB.poiv = 'b'
JOIN YourTable ytC
on ytB.Order_id = ytC.Order_id
AND ytC.poi = 'C'
AND ytC.poiv = 'c'
where
ytA.poi = 'A'
AND ytA.poiv = 'a'
find the "intersection" of lists, each of which contains one set
select id
from
(select id from mytable where poi = 'A' and povi= 'a') t1
inner join
(select id from mytable where poi = 'B' and povi= 'b') t2
using(id)
inner join
(select id from mytable where poi = 'C' and povi= 'c') t3
using(id)
demo

Select unique row using several columns unique combination excluding not applicable values

I have a table with three columns, lets define it and fill it with sample data:
A | B | C |
------ | ----- | ------|
A1 | NA | NA |
A1 | B1 | NA |
NA | B1 | NA |
NA | B1 | C1 |
NA | NA | C1 |
As we can see combination (A,B,C) is unique. I need to have a select which would select exact one row using parameters :A :B :C. For example:
1) If I have :A = A1, :B = SOME_B, :C = SOME_C. It should select first row because column A has exact match while columns B and C are not applicable.
2) If I have :A = SOME_A, :B = SOME_B, :C = SOME_C. It should not return anything.
First try was:
SELECT *
FROM SAMPLE_TABLE
WHERE (A = :A OR A = 'NA') AND (B = :B OR B = 'NA') AND (C = :C OR C = 'NA');
This returned first two rows. Wrong, as expected. That is not what I want.
Second try:
SELECT *
FROM SAMPLE_TABLE
WHERE (A = :A OR (A = 'NA' AND NOT EXISTS(SELECT 1 FROM SAMPLE_TABLE WHERE A = :A AND (B = :B OR B = 'NA') (C = :C OR C = 'NA')) AND (B = :B OR B = 'NA') AND (C = :C OR C = 'NA');
It works and logic is correct, but for B and C I need to do same stuff. After that I need to modify NOT EXISTS for column A which should include NOT EXISTS for B and C, which also should include NOT EXISTS for column A and so on. I get a loop.
Any ideas how can we write simple SQL for solving such problem? Or I just miss some silly thing?
EDIT:
Oh, it's my fault, wasn't able to be clear enought. Let's look into business example (table TEAM_MATRIX):
COUNTRY|PRIORIT| TEAM |
------ | ----- | ------|
NA | NA | GLOB_T|
UK | LOW | UK_T1 |
UK | MED | UK_T2 |
UK | HIGH | UK_T3 |
US | NA | US_T |
US | LOW | US_T1 |
Coulmns country and priority can be treated as input parameters. Column team should be treated as output. Several examples:
1) Suppose we have parameters: country = FR, priority = LOW. Then output team should be GLOB_T because there is no such combination FR and LOW. (NA means that we do not care).
2) If country is US and priority is LOW => ouput team should be US_T1 because we have combination US and LOW.
3) If country is US and priority is MED => output team should be US_T because we have combination US and NA (NA means that we do not care).
Is there a way to have SQL which follows this logic?
SELECT TEAM
FROM TEAM_MATRIX
WHERE (COUNTRY = ?1 OR COUNTRY = 'NA') AND (PRIORIT = ?2 OR PRIORIT = 'NA');
Here ?1 - country parameter, ?2 - priority parameter. This does not work, for example combination ((NA, NA) -> GLOB_T) will always be selected.
Thanks!

SQL Aggregation with SUM, GROUP BY and JOIN (many-to-many)

Here's an example Table layout:
TABLE_A: TABLE_B: TABLE_A_B:
id | a | b | c id | name a_id | b_id
--------------------- --------- -----------
1 | true | X | A 1 | A 1 | 1
2 | true | Z | null 2 | B 1 | 2
3 | false | X | null 3 | C 2 | 2
4 | true | Y | Q 4 | 1
5 | false | null | null 4 | 2
5 | 1
Possible Values:
TABLE_A.a: true, false
TABLE_A.b: X, Y, Z
TABLE_A.c: A, B, C, ... basically arbitrary
TABLE_B.name: A, B, C, ... basically arbitrary
What I want to achieve:
SELECT all rows from TABLE_A
SUM(where a = true),
SUM(where a = false),
SUM(where b = 'X'),
SUM(where b = 'Y'),
SUM(where b = 'Z'),
SUM(where b IS NULL),
and also get the SUMs for all distinct TABLE_A.c values.
and also get the SUMs for all those TABLE_A_B relations.
The result for the example Table above should look like:
aTrue | aFalse | bX | bY | bZ | bNull | cA | cQ | cNull | nameA | nameB | nameC
-------------------------------------------------------------------------------
3 | 2 | 2 | 1 | 1 | 1 | 1 | 1 | 3 | 3 | 3 | 0
What I've done so far:
SELECT
SUM(CASE WHEN a = true THEN 1 ELSE 0 END) AS aTrue,
SUM(CASE WHEN b = false THEN 1 ELSE 0 END) AS aFalse,
SUM(CASE WHEN b = 'X' THEN 1 ELSE 0 END) AS bX,
...
FROM TABLE_A
What's my problem?
Selecting column TABLE_A.a and TABLE_A.b is easy, because there's a fixed number of possible values.
But I can't figure out how to count the distinct values of TABLE_A.c. And basically the same problem for the JOINed TABLE_B, because the number of values within TABLE_B is unknown and can change over time.
Thanks for your help! :)
EDIT1: New (preferred) SQL result structure:
column | value | sum
----------------------------
TABLE_A.a | true | 3
TABLE_A.a | false | 2
TABLE_A.b | X | 2
TABLE_A.b | Y | 1
TABLE_A.b | Z | 1
TABLE_A.b | null | 1
TABLE_A.c | A | 1
TABLE_A.c | Q | 1
TABLE_A.c | null | 3
TABLE_B.name | A | 3
TABLE_B.name | B | 3
TABLE_B.name | C | 0
From your original request of rows as a simulated pivot. By doing a SUM( logical condition ) basically returns 1 if true, 0 if false. So, since the column "a" is true or false, simple sum of "a" or NOT "a" (for the false counts -- NOT FALSE = TRUE). Similarly, your "b" column, so b='X' = true counted as 1, else 0.
In other sql engines, you might see it as SUM( case/when ).
Now, since your table counts don't rely on each other, they can be separate SUM() into their own sub-alias query references (pqA and pqB for pre-queryA and pre-queryB respectively). Since no group by, they will each result in a single row. With no join will create a Cartesian, but since 1:1 ratio, will only return a single record of all columns you want.
SELECT
pqA.*, pqB.*
from
( SELECT
SUM( ta.a ) aTrue,
SUM( NOT ta.a ) aFalse,
SUM( ta.b = 'X' ) bX,
SUM( ta.b = 'Y' ) bY,
SUM( ta.b = 'Z' ) bZ,
SUM( ta.b is null ) bNULL,
SUM( ta.c = 'A' ) cA,
SUM( ta.c = 'Q' ) cQ,
SUM( ta.c is null ) cNULL,
COUNT( distinct ta.c ) DistC
from
table_a ta ) pqA,
( SELECT
SUM( b.Name = 'A' ) nameA,
SUM( b.Name = 'B' ) nameB,
SUM( b.Name = 'C' ) nameC
from
table_a_b t_ab
join table_b b
ON t_ab.b_id = b.id ) pqB
This option gives your second (preferred) output
SELECT
MAX( 'TABLE_A.a ' ) as Basis,
CASE when a then 'true' else 'false' end Value,
COUNT(*) finalCnt
from
TABLE_A
group by
a
UNION ALL
SELECT
MAX( 'TABLE_A.b ' ) as Basis,
b Value,
COUNT(*) finalCnt
from
TABLE_A
group by
b
UNION ALL
SELECT
MAX( 'TABLE_A.c ' ) as Basis,
c Value,
COUNT(*) finalCnt
from
TABLE_A
group by
c
UNION ALL
SELECT
MAX( 'TABLE_B.name ' ) as Basis,
b.Name Value,
COUNT(*) finalCnt
from
table_a_b t_ab
join table_b b
ON t_ab.b_id = b.id
group by
b.Name
I think You will need to build dynamic query as you don't know possible values for column C in table A. So you can write store procedure where you can get list of distinct value for Column C in one variable and by using "Do WHILE" you can construct your dynamic query.
Please let me know if you need more help in detail
Dynamic SQL

Mysql search on multiple join results

I've a table "products" and a table where are store some attributes of a product:
zd_products
----------
|ID|title|
----------
| 1| Test|
| 2| Prod|
| 3| Colr|
zd_product_attached_attributes
------------------
|attrid|pid|value|
------------------
|1 | 1 | A |
|2 | 1 | 10 |
|3 | 1 | AB |
|1 | 2 | B |
|2 | 2 | 22 |
|3 | 2 | BB |
|1 | 3 | A |
|2 | 3 | 10 |
|3 | 3 | CC |
I want to search in zd_products only the products that have some attributes values, for exam place
Get the product when the attribute 1 is A and the attribute 3 is AB
Get the product when the attribute 2 is 10 and the attribute 3 is CC
etc
How can i do this using a join ?
Oh, the Joys of the EAV model!
One way is to use a separate JOIN operation for each attribute value. For example:
SELECT p.id
, p.title
FROM zd_products p
JOIN zd_product_attached_attributes a1
ON a1.pid = p.id
AND a1.attrid = 1
AND a1.value = 'A'
JOIN zd_product_attached_attributes a3
ON a3.pid = p.id
AND a3.attrid = 3
AND a3.value = 'AB'
With appropriate indexes, that's likely going to be the most efficient approach. This isn't the only query that will return the specified result, but this one does make use of JOIN operations.
Another, less intuitive approach
If id is unique in the zd_products table, and we have guarantee that the (attrid,pid,value) tuple is unique in the zd_product_attached_attributes table, then this:
SELECT p.id
, p.title
FROM zd_products p
JOIN zd_product_attached_attributes a
ON a.pid = p.id
AND ( (a.attrid = 1 AND a.value = 'A')
OR (a.attrid = 3 AND a.value = 'AB')
)
GROUP
BY p.id
, p.title
HAVING COUNT(1) > 1
will return an equivalent result. The latter query is of a form that is particularly suitable for matching two criteria out of three, where we don't need a match on ALL of the attributes, but just some of them. For example, finding a product that matches any two of:
color = 'yellow'
size = 'bigger'
special = 'on fire'
And of course there are other approaches that don't make use of a JOIN.
FOLLOWUP
Q: And if I want to the same but using OR operator? I mean get ONLY if the attribute 1 is A or the attribute 2 is AB otherwise don't select the record.
A: A query of the form like the second one in my answer (above) is more conducive to the OR condition.
If you want XOR (exclusive OR), where one of the attributes has a matching value but the other one doesn't, just change the HAVING COUNT(1) > 1 to HAVING COUNT(1) = 1. Only rows from products that find one "matching" row in the attributes table will be returned. To match exactly 2 (out of several), HAVING COUNT(1) = 2, etc.
A query like the first one in my answer can be modified to use OUTER joins, to find matches, and then do a conditional test in the WHERE clause, to determine if a match was found.
SELECT p.id
, p.title
FROM zd_products p
LEFT
JOIN zd_product_attached_attributes a1
ON a1.pid = p.id
AND a1.attrid = 1
AND a1.value = 'A'
LEFT
JOIN zd_product_attached_attributes a3
ON a3.pid = p.id
AND a3.attrid = 3
AND a3.value = 'AB'
WHERE a1.pid IS NOT NULL
OR a3.pid IS NOT NULL
I've just added the LEFT keyword, to specify an outer join; rows from products will be returned with matching rows from a1 and a3, along with rows from products that don't have any matching rows found in a1 or a3.
The WHERE clause tests a column from a1 and a3 to see whether a matching row was returned. If a matching row was found in a1, we are guaranteed that the pid column from a1 will be non-NULL. That column will be returned as NULL only if a matching row was not found.
If we replaced the OR with an AND, we'd be negating the "outerness" of both joins, making it essentially equivalent to the first query above.
To get an XOR type operation (exclusive OR) where we find one matching attribute but not the other, we could change the WHERE clause to read:
WHERE (a1.pid IS NOT NULL AND a3.pid IS NULL)
OR (a3.pid IS NOT NULL AND a1.pid IS NULL)
Use a pivot
You can do this type of query using a pivot. As far as I know, MySQL doesn't have a native, built in pivot, but you can achieve this by transposing the rows and columns of your zd_product_attached_attributes table using:
SELECT pid,
MAX(CASE WHEN attrid = 1 THEN value END) `attrid_1`,
MAX(CASE WHEN attrid = 2 THEN value END) `attrid_2`,
MAX(CASE WHEN attrid = 3 THEN value END) `attrid_3`
FROM zd_product_attached_attributes
GROUP BY pid
This will pivot your table as shown:
+----+---------+-------+ +----+----------+----------+----------+
| attrid | pid | value | | pid| attrid_1 | attrid_2 | attrid_3 |
+----+---+-------------+ +----+----------+----------+----------+
| 1 | 1 | A | | 1 | A | 10 | AB |
| 2 | 1 | 10 | => | 2 | B | 22 | BB |
| 3 | 1 | AB | | 3 | A | 10 | CC |
| 1 | 2 | B | +----+----------+----------+----------+
| 2 | 2 | 22 |
| 3 | 2 | BB |
| 1 | 3 | A |
| 2 | 3 | 10 |
| 3 | 3 | CC |
+--------+---------+---+
So you can select the products id and title using:
SELECT id, title FROM zd_products
LEFT JOIN
(
SELECT pid,
MAX(CASE WHEN attrid = 1 THEN value END) `attrid_1`,
MAX(CASE WHEN attrid = 2 THEN value END) `attrid_2`,
MAX(CASE WHEN attrid = 3 THEN value END) `attrid_3`
FROM zd_product_attached_attributes
GROUP BY pid
) AS attrib_search
ON id = pid
WHERE ( attrib_1 = 'A' AND attrib_3 = 'AB' )
OR ( attrib_2 = 10 AND attrib_3 = 'CC' )
Note: You can use this type of query when you have guaranteed uniqueness on (pid, attrid)
(thanks #spencer7593)
I haven't tested this, but I think it should work:
select title
from zd_products p
join zd_product_attached_attributes a ON a.pid = p.id
where ( attrid = 1 and value = 'A' )
or ( attrid = 3 and value = 'AB' );
If you want to tack on more "searches" you could append more lines similar to the last one (ie. or "or" statements)