Complex SQL Query: Select like tree traversal - mysql

Consider the following (1:N) relationship:
[entity: user] <------ rid key ------> [entity: rid].
consider the data in both tables as:
select * from user;
user-id rid-key
a-basa a
b-basa b
a.a-basa a.a
a.b-basa a.b
a.a.a-basa a.a.a
a.a.b-basa a.a.b
a.b.a-basa a.b.a
a.b.b-basa a.b.b
a.b.b.a-basa a.b.b.a
a.b.b.b-basa a.b.b.b
select * from rid;
rid-key parent-rid enabled
a null true
b null true
a.a a true
a.b a false
a.a.a a.a true
a.b.a a.b true
a.b.b a.b true
a.b.b.a a.b.b true
......
n rows
I need to design a single query (not stored procedure) which will input a user-id, and the following facts are considered:
If an user is given access to a rid, then it can also access the parent rid of the rid given - the rid itself is enabled (enabled = true).
This should continue till we reach the root rid, ie. parent rid property is null.
In above example, the list of accessible rid for the user 'a.b.b.a-basa' will be:
a.b.b.a
a.b.b
a.b
and for a.a.a-basa:
a.a.a
a.a
a
can we get this list using a single query? Any sql vendor is fine.

There are several models for Hierarchical data. Most models (like the Adjacency List) require some sort of recursion for some queries. With your design that uses the Materialized Path model, what you want is possible without a recursive query.
Tested in MySQL (that has no recursive queries), at SQL-fiddle test-mysql. It can be easily converted for other DBMS, if you modify the string concatenation part:
SELECT
COUNT(*)-1 AS steps_up,
rid2.rid_key AS ancestor_rid_key
FROM
u2
JOIN
rid
ON u2.rid_key = rid.rid_key
OR u2.rid_key LIKE CONCAT(rid.rid_key, '.%')
JOIN
rid AS rid2
ON rid.rid_key = rid2.rid_key
OR rid.rid_key LIKE CONCAT(rid2.rid_key, '.%')
WHERE
u2.userid = 'basa'
AND
u2.rid_key = 'a.b.b.a'
GROUP BY
rid2.rid_key, rid2.enabled
HAVING
COUNT(*) + (rid2.enabled = 'true')
= SUM(rid.enabled = 'true') + 1 ;
It uses this view, which is not strictly needed but it shows that the user.user_id is storing data that you already have in the rid_key column.
CREATE VIEW u2 AS
SELECT
SUBSTRING_INDEX(user_id, '-', -1) AS userid
, rid_key
FROM user ;
One more note is that the above query does not use the parent_rid column at all. And that I'm sure it can be further improved.

In Oracle, you can achieve this using a hierarhical query. Search for CONNECT BY or have a look at this article.

This should get the ball rolling for you.
The answer works on SQL Server 2005 onwards
DECLARE #UsersRIDkey VARCHAR(10) = 'a.a.a'
;WITH UserCTE (userid, ridkey) AS
(
SELECT 'a-basa', 'a' UNION ALL
SELECT 'b-basa', 'b' UNION ALL
SELECT 'a.a-basa', 'a.a' UNION ALL
SELECT 'a.b-basa', 'a.b' UNION ALL
SELECT 'a.a.a-basa', 'a.a.a' UNION ALL
SELECT 'a.a.b-basa', 'a.a.b' UNION ALL
SELECT 'a.b.a-basa', 'a.b.a' UNION ALL
SELECT 'a.b.b-basa', 'a.b.b' UNION ALL
SELECT 'a.b.b.a-basa', 'a.b.b.a' UNION ALL
SELECT 'a.b.b.b-basa', 'a.b.b.b'
)
,RidCTE (ridkey, parentrid, isenabled) AS
(
SELECT 'a', null, 1 UNION ALL
SELECT 'b', null, 1 UNION ALL
SELECT 'a.a', 'a', 1 UNION ALL
SELECT 'a.b', 'a', 0 UNION ALL
SELECT 'a.a.a', 'a.a', 1 UNION ALL
SELECT 'a.b.a', 'a.b', 1 UNION ALL
SELECT 'a.b.b', 'a.b', 1 UNION ALL
SELECT 'a.b.b.a', 'a.b.b', 1
)
,RidHierarchyCTE AS
(
SELECT *
FROM RidCTE
WHERE ridkey = #UsersRIDkey
UNION ALL
SELECT R.ridkey, R.parentrid, R.isenabled
FROM RidHierarchyCTE H
JOIN RidCTE R ON R.ridkey = H.parentrid
)
SELECT ridkey
FROM RidHierarchyCTE

Oracle solution:
SQL> select u.user_id, r.rid_key, r.parent_rid, r.enabled
2 from users u
3 inner join rid r
4 on r.rid_key = u.rid_key
5 start with u.user_id = 'a.a.a-basa'
6 connect by prior r.parent_rid = r.rid_key and prior enabled = 'true'
7 /
USER_ID RID_KEY PAREN ENABL
------------ ------- ----- -----
a.a.a-basa a.a.a a.a true
a.a-basa a.a a true
a-basa a null true
SQL> select u.user_id, r.rid_key, r.parent_rid, r.enabled
2 from users u
3 inner join rid r
4 on r.rid_key = u.rid_key
5 start with u.user_id = 'a.b.b.a-basa'
6 connect by prior r.parent_rid = r.rid_key and prior enabled = 'true'
7 /
USER_ID RID_KEY PAREN ENABL
------------ ------- ----- -----
a.b.b.a-basa a.b.b.a a.b.b true
a.b.b-basa a.b.b a.b true
a.b-basa a.b a false
http://sqlfiddle.com/#!4/d529f/1

Related

check for two values in a table

I have a simple table where I store some data related to a user.
id
id_user
id_func
1
1
1
2
1
2
3
2
1
I want to check if a specific user has two specific functions (1 AND 2) assigned. So If I check for user 1 the query will return true. For user 2 it will return false.
The query I have so far is:
SELECT IF(
"SELECT id_user FROM funz_abilitate WHERE id_func=1 AND id_func=2 AND id_user=1",
true,
false
) as verifica
FROM funz_abilitate
WHERE id_user=1
but this is returning for user 1:
verifica
0
0
and for user 2:
verifica
0
While I'd like just to get a boolean true/false. So if I select user 1 it returns true, if I select user 2 it returns false
How to fix it?
You can do it with aggregation:
SELECT COUNT(*) = 2 AS verifica
FROM funz_abilitate
WHERE id_user = 1 AND id_func IN (1, 2)
or:
SELECT COUNT(DISTINCT id_func) = 2 AS verifica
FROM funz_abilitate
WHERE id_user = 1 AND id_func IN (1, 2)
if there are duplicate id_funcs for each user.
In MySql 8.0+ I would use a CTE which returns the id_funcs I search for, so there is no need to write how many they are:
WITH cte(id_func) AS (VALUES ROW(1), ROW(2))
SELECT COUNT(*) = (SELECT COUNT(*) FROM cte) AS verifica
FROM funz_abilitate
WHERE id_user = 1 AND id_func IN (SELECT id_func FROM cte)
See the demo.

SQL - Get rows where all columns are in a list that have the same value in another column

I have a database as follows:
drinks_id ingredients_master_id
1 2
1 3
1 4
2 2
2 4
3 5
And I'm looking for a query where I can give it a list of ingredients_master_id such as 2,4 and it returns all of the drinks_id's that have exactly 2,4.
So in this case if I gave it ingredients_master_id 2,4,5 it would return drinks_id 2 and 3. And if I gave it 5 it would return drinks_id 3.
This is what I have so far but it's currently not displaying the correct info.
SELECT DISTINCT drinks.id
FROM drinks
WHERE drinks.id NOT IN
(
SELECT drinks.id
FROM ingredients
JOIN drinks ON ingredients.drinks_id = drinks.id
WHERE ingredients.ingredients_master_id NOT IN
(
2,3,4,5,6
)
);
You probably achieve the desired result using not exists as follows:
Select t.*
From your_table t
Where t.ingredients_master_id in (2,4,5)
And not exists
(Select 1 from your_table tt
Where tt.drinks_id = t.drinks_id
And tt.ingredients_master_id not in (2,4,5))
And I'm looking for a query where I can give it a list of ingredients_master_id such as 2,4 and it returns all of the drinks_id's that have exactly 2,4.
You can use group by and having:
select drinks_id
from t
group by drinks_id
having sum( ingredients_master_id in (2, 4) ) = 2;
The "= 2" is the size of the list, so you need to adjust that for different lists.
You can use as below:
select drink_id
from (
select drink_id,listagg(ingredients_master_id,',') within group( order by ingredients_master_id) val from <Table> group by drink_id)
where case
when instr('**2,4,5**',val)> 0
Then 'Y'
else 'N'
end = 'Y';

MySQL Request with IN clause, with array of couple identifier

I have a user table with a couple as identifier : id and type, like this :
id | type | name
----------------
15 | 1 | AAA
16 | 1 | BBB
15 | 2 | CCC
I would like to get a list, matching both id and type.
I currently use a concat system, which works :
SELECT u.id,
u.type,
u.name
FROM user u
WHERE CONCAT(u.id, '-', u.type) IN ('15-1', '16-1', '17-1', '10-2', '15-2')
But, I have the feeling it could be better, what would be the proper way to do it ?
Thank you !
You may use the following approach in mysql
with dat as
(
select 17 id, 1 type, 'AAA' t
union all
select 16 id, 1 type, 'BBB' t
union all
select 17 id, 2 type, 'CCC' t
)
-- end of testing data
select *
from dat
where (id, type) in (
-- query data
(17, 1), (16, 1)
)
IN can operate on "tuples" of values, like this (a, b) IN ((c,d), (e,f), ....). Using this method is (should be) faster as you are not doing a concat operation on "a" and "b" and then comparing strings; instead you are comparing pairs of values, unprocessed and with an appropriate comparison operation (i.e. not always string compares).
Additionally, if "a" and/or "b" are string values themselves using the concat technique risks ambiguous results. ("1-2","3") and ("1","2-3") pairs concat to the same result "1-2-3"
You can separate them out. Not sure if it's more efficient but at least you would save the concat part :
SELECT u.id,
u.type,
u.name
FROM user u
WHERE (u.id = 15 AND u.type = 1)
OR (u.id = 16 AND u.type = 1)
OR (u.id = 17 AND u.type = 1)
OR (u.id = 10 AND u.type = 2)
OR (u.id = 15 AND u.type = 2)
I think it depends a lot on how you obtain the values for id and type that you use for filtering
If they are results of another computation they can be saved in a temporary table and used in a join
create TEMPORARY TABLE criteria as
select 15 as id, 1 as type
UNION
select 16 as id, 1 as type
UNION
select 17 as id, 1 as type
UNION
select 10 as id, 2 as type
UNION
select 15 as id, 2 as type
SELECT u.id,
u.type,
u.name
FROM user u
inner join criteria c on u.type = c.type and u.id = c.id
The other option is an inner query and then a join or a WITH clause (which is rather late addition to Mysql arsenal of tricks)

MYSQL counting occurrences separately from 2 tables

I got 3 tables in my MYSQL bases and I have to compare how many time there are each user_ID in each of the 2 first table (table 1 and table 2)
here is my table 1:
user_ID
A
B
A
D
...
here is my table 2 :
user_ID
A
C
A
...
here is my table 3 (with link between user_ID and nickname) :
user_ID // nickname
A // Bob
B // Joe
C // Tom
...
I would like to get a result like this:
Nickname // count occurrences from Table 1 // count occurrences from table 2
Bob // 1 // 2
Joe // 4 // 0
Tom // 0 // 2
I did not succeed for instant to count separately from each table, I got a global result for each nickname :(
Could you help me to find the right MYSQL request ?
- ...
This type of query is a little tricky, because some names may not be in the first table and others may not be in the second. To really solve this type of problem, you need to pre-aggregate the results for each query. To get all the names, you need a left outer join:
select t3.name, coalesce(cnt1, 0) as cnt1, coalesce(cnt2, 0) as cnt2
from table3 t3 left outer join
(select name, count(*) as cnt1
from table1
group by name
) t1n
on t3.name = t1n.name left outer join
(select name, count(*) as cnt2
from table2
group by name
) t2n
on t3.name = t1n.name;

SQL query problems transpose MySQL

There are 2 simple tables
People:
person_id
Name
Reading assestment
date
person_id
quality
speed
Trying to create sql query
SELECT
AVG(r.quality),
AVG(r.speed),
FROM reading_assestment r,people p
where r.person_id =p.person_id
and person_id="3"
Current output:
Quality Speed
77 65
Outcome I am looking for:
Assestment Value
Quality 77
Speed 65
It is something to do with transpose , pivots.
The most general way is to start with your query and then unpivot using separate logic:
select (case when n = 1 then 'Quality' else 'Speed' end) as Assessment,
(case when n = 1 then avg_quality else avg_speed end) as Value
from (select AVG(r.quality) as avg_quality, AVG(r.speed) as avg_speed
from reading_assestment r join
people p
on r.person_id =p.person_id
where person_id = 3
) t cross join
(select 1 as n union all select 2) n
SELECT
AggResults.person_id AS person_id,
Assesment.Name AS AssessmentName,
CASE WHEN Assessment.Name = 'Quality' THEN AggResults.AvgQuality
WHEN Assessment.Name = 'Speed' THEN AggResults.AvgSpeed
ELSE NULL
END AS AssessmentValue
FROM
(
SELECT
people.person_id AS person_id,
AVG(quality) AS AvgQuality,
AVG(speed) AS AvgSpeed
FROM
reading_assessment
INNER JOIN
people
ON reading_assessment.person_id = people.person_id
GROUP BY
people.person_id
)
AS AggResults
CROSS JOIN
(
SELECT 'Quality' AS Name
UNION ALL
SELECT 'Speed' AS Name
)
AS Assessment
WHERE
AggResults.person_id = 3
I moved the = 3 to the outer query, just for ease of use. Most optimisers will treat both options similarly.