Matching Exactly all values in IN clause - mysql

I am searching for the solution for this problem for hours now with no luck. I have a Workouts table as below. Each item in the workout table can have multiple target muscles, which are listed in the Target Muscles table.
Workouts table:
id
1
2
Target Muscles table:
id
muscle_key
workout_id
1
a
1
2
b
1
3
c
1
4
a
2
5
b
2
I need to fetch all items in the workouts table which match EXACTLY ALL target muscles keys in the given set, not less and not more. For example, given the set of muscle keys:
(a,b)
The desired output would be:
id
2
The row for workout id = 1 should NOT be selected since it contains an extra muscle key (c).
I am using the following query:
SELECT id
FROM workouts
LEFT JOIN target_muscles ON workouts.id = target_muscles.workout_id
WHERE target_muscles.muscle_key IN (a,b)
GROUP BY workouts.id
HAVING COUNT(DISTINCT target_muscles.muscle_key) = 2
The above query is also returning the workout id = 1, instead of only 2. How can I achieve this?
Any help is appreciated.

Skip the WHERE clause. Use HAVING to make sure exactly a and b are there.
SELECT workouts.id
FROM workouts
JOIN target_muscles ON workouts.id = target_muscles.workout_id
GROUP BY workouts.id
HAVING COUNT(DISTINCT target_muscles.muscle_key) =
COUNT(DISTINCT CASE WHEN target_muscles.muscle_key IN (a,b)
THEN target_muscles.muscle_key END)
AND COUNT(DISTINCT target_muscles.muscle_key) = 2
Can also be done as:
SELECT workouts.id
FROM workouts
JOIN target_muscles ON workouts.id = target_muscles.workout_id
GROUP BY workouts.id
HAVING MIN(target_muscles.muscle_key) = 'a'
AND MAX(target_muscles.muscle_key) = 'b'
AND COUNT(DISTINCT target_muscles.muscle_key) = 2
Or, perhaps less performant:
SELECT workouts.id
FROM workouts
JOIN (SELECT workout_id FROM target_muscles WHERE muscle_key = 'a'
INTERSECT
SELECT workout_id FROM target_muscles WHERE muscle_key = 'b'
EXCEPT
SELECT workout_id FROM target_muscles WHERE muscle_key NOT IN ('a', 'b')) dt
ON workouts.id = dt.workout_id

You can remove the filtering clause and use two conditions:
count of non-(a,b) muscle_keys = 0
distinct count of (a,b) muscle_keys = 2
SELECT w.id
FROM workouts w
LEFT JOIN target_muscles ts ON w.id = ts.workout_id
GROUP BY w.id
HAVING COUNT(CASE WHEN ts.muscle_key NOT IN ('a', 'b') THEN w.id END) = 0
AND COUNT(DISTINCT CASE WHEN ts.muscle_key IN ('a', 'b') THEN ts.muscle_key END) = 2
Check the demo here.

This is an other way to do it using inner join
select distinct s.id
from (
select w.id
from workouts w
inner join targets t on t.workout_id = w.id
group by workout_id
having count(1) = 2
) as s
inner join targets t on t.workout_id = s.id and t.muscle_key in ('a', 'b');
You can Try it from here : https://dbfiddle.uk/nPTQlhqT

Related

MySQL select with group and one to many relations condition

For example have such structure:
CREATE TABLE clicks
(`date` varchar(50), `sum` int, `id` int)
;
CREATE TABLE marks
(`click_id` int, `name` varchar(50), `value` varchar(50))
;
where click can have many marks
So example data:
INSERT INTO clicks
(`sum`, `id`, `date`)
VALUES
(100, 1, '2017-01-01'),
(200, 2, '2017-01-01')
;
INSERT INTO marks
(`click_id`, `name`, `value`)
VALUES
(1, 'utm_source', 'test_source1'),
(1, 'utm_medium', 'test_medium1'),
(1, 'utm_term', 'test_term1'),
(2, 'utm_source', 'test_source1'),
(2, 'utm_medium', 'test_medium1')
;
I need to get agregated values of click grouped by date which contains all of selected values.
I make request:
select
c.date,
sum(c.sum)
from clicks as c
left join marks as m ON m.click_id = c.id
where
(m.name = 'utm_source' AND m.value='test_source1') OR
(m.name = 'utm_medium' AND m.value='test_medium1') OR
(m.name = 'utm_term' AND m.value='test_term1')
group by date
and get 2017-01-01 = 700, but I want to get 100 which means that only click 1 has all of marks.
Or if condition will be
(m.name = 'utm_source' AND m.value='test_source1') OR
(m.name = 'utm_medium' AND m.value='test_medium1')
I need to get 300 instead of 600
I found answer in getting distinct click_id by first query and then sum and group by date with condition whereIn, but on real database which is very large and has id as uuid this request executes extrimely slow. Any advices how to get it work propely?
You can achieve it using below queries:
When there are the three conditions then you have to pass the HAVING count(*) >= 3
SELECT cc.DATE
,sum(cc.sum)
FROM clicks AS cc
INNER JOIN (
SELECT id
FROM clicks AS c
LEFT JOIN marks AS m ON m.click_id = c.id
WHERE (
m.NAME = 'utm_source'
AND m.value = 'test_source1'
)
OR (
m.NAME = 'utm_medium'
AND m.value = 'test_medium1'
)
OR (
m.NAME = 'utm_term'
AND m.value = 'test_term1'
)
GROUP BY id
HAVING count(*) >= 3
) AS t ON cc.id = t.id
GROUP BY cc.DATE
When there are the three conditions then you have to pass the HAVING count(*) >= 2
SELECT cc.DATE
,sum(cc.sum)
FROM clicks AS cc
INNER JOIN (
SELECT id
FROM clicks AS c
LEFT JOIN marks AS m ON m.click_id = c.id
WHERE (
m.NAME = 'utm_source'
AND m.value = 'test_source1'
)
OR (
m.NAME = 'utm_medium'
AND m.value = 'test_medium1'
)
GROUP BY id
HAVING count(*) >= 2
) AS t ON cc.id = t.id
GROUP BY cc.DATE
Demo: http://sqlfiddle.com/#!9/fe571a/35
Hope this works for you...
You're getting 700 because the join generates multiple rows for the different IDs. There are 3 rows in the mark table with ID=1 and sum=100 and there are two rows with ID=2 and sum=200. On doing the join where shall have 3 rows with sum=100 and 2 rows with sum=200, so adding these sum gives 700. To fix this you have to aggregate on the click_id too as illustrated below:
select
c.date,
sum(c.sum)
from clicks as c
inner join (select * from marks where (name = 'utm_source' AND
value='test_source1') OR (name = 'utm_medium' AND value='test_medium1')
OR (name = 'utm_term' AND value='test_term1')
group by click_id) as m
ON m.click_id = c.id
group by c.date;
DEMO SQL FIDDLE
I found the right way myself, which works on large amounts of data
The main goal is to make request generate one table with subqueries(conditions) which do not depend on amount of data in results, so the best way is:
select
c.date,
sum(c.sum)
from clicks as c
join marks as m1 ON m1.click_id = c.id
join marks as m2 ON m2.click_id = c.id
join marks as m3 ON m3.click_id = c.id
where
(m1.name = 'utm_source' AND m1.value='test_source1') AND
(m2.name = 'utm_medium' AND m2.value='test_medium1') AND
(m3.name = 'utm_term' AND m3.value='test_term1')
group by date
So we need to make as many joins as many conditions we have

How can I accomplish the following in SQL?

I have two tables.
table_a:
id | data_x | data_y
--------------------
1 person joe
2 person bob
3 amount 200
4 addres philville
tableB:
map_id | table_a_id
-------------------
7 1
7 3
7 4
8 4
8 2
The result I want is the map_id if it has an entry in table_a for both data_x = 'person' and data_y = '200'
So with the above table B, the result should be
map_id
------
7
How can I write that query in SQL?
This situation is a perfect fit for an unusual SQL operator: INTERSECT. It is a very declarative, efficient and elegant solution for this problem.
SELECT Map.map_id
FROM Table_B AS Map JOIN Table_A AS Person ON (Person.id = Map.table_a_id) AND (Person.data_x = 'person')
INTERSECT
SELECT Map.map_id
FROM Table_B AS Map JOIN Table_A AS Amount ON (Amount.id = Map.table_a_id) AND (Amount.data_y = '200')
Formally what you are asking for is exactly the intersection of two disjoint sets: the set of map id's that are persons and the set of map id's that have a value of 200.
Please note the INTERSECT operator does not exists in MySQL, but it does in almost all advanced relational DBMS, including PostgreSQL.
This is less elegant than the INTERSECT solution #Malta posted, but it works with the limited capabilities of MySQL as well:
SELECT b1.map_id
FROM table_a a1
JOIN tableb b1 ON a1.id = b1.table_a_id AND a1.data_x = 'person'
JOIN tableb b2 ON b2.map_id = b1.map_id AND b2.table_a_id <> b1.table_a_id
JOIN table_a a2 ON a2.id = b2.table_a_id AND a2.data_y = '200';
SQL Fiddle for MySQL.
SQL Fiddle for Postgres.
Based on your input, the following should get you started using MySQL:
SELECT
map_id
FROM TableB
JOIN Table_A
ON TableB.table_a_id = Table_A.id
AND
((Table_A.data_x = 'person')
OR
(Table_A.data_y = '200')
)
GROUP BY map_id
HAVING COUNT(table_a_id) = 2
;
See it in action: SQL Fiddle.
Update
As Erwin Brandstetter made explicit: If the data can't be trusted to be inherently consistent (along the lines of your inquiry), one option is:
SELECT map_id FROM (
SELECT map_id, 'data_x' t
FROM TableB B JOIN Table_A A ON B.table_a_id = A.id AND A.data_x = 'person'
UNION
SELECT map_id, 'data_y'
FROM TableB B JOIN Table_A A ON B.table_a_id = A.id AND A.data_y = '200'
) T
GROUP BY map_id
HAVING COUNT(DISTINCT t) = 2
;
This should ensure "at least one each". (Alternatives have been suggested by others.) To get "exactly one each", you could try
SELECT map_id FROM (
SELECT map_id, 'data_x' t, data_y
FROM TableB B JOIN Table_A A ON B.table_a_id = A.id AND A.data_x = 'person'
UNION
SELECT map_id, 'data_y', data_y
FROM TableB B JOIN Table_A A ON B.table_a_id = A.id AND A.data_y = '200'
) T
GROUP BY map_id
HAVING COUNT(DISTINCT t) = 2 AND COUNT(DISTINCT data_y) = 2
;
See it in action (with additional test data): SQL Fiddle.
And it works in PostgreSQL as well: SQL Fiddle
Please comment if and as this requires adjustment / further detail.
Join the 2 tables, group by map_id, use conditional counting with either count() or sum(), and filter in having clause (I use mysql syntax below):
select map_id,
sum(
case
when a.data_x='person' or a.data_y='200' then 1
else 0
end
) as matches
from a
inner join b on a.id=b.a_id
group by b.map_id
having matches=2
The above query assumes that you cannot have more than one record for any map_id where data_x is person or data_y is 200. If this assumption is incorrect, then you need to use either exists subqueries or 2 derived tables.
Sounds like you want a standard INNER JOIN.
But I do beg to differ on your result:
map_id if it has an entry in table_a for both data_x = 'person' and data_y = '200'
There is not a record in your data set that has both 'person' and data_y = '200' and therefore no mp_id can be returned
Here is a typical INNER JOIN relating to your narrative.
SELECT DISTINCT
b.map_id
FROM
TableA a
INNER JOIN TableB b
ON a.id = b.table_a_id
WHERE
a.data_x = 'person'
AND a.data_y = '200'
If more than one map_id exists with data_x = 'person' and data_y = '200' then you will get multiple results but only 1 row per map_id
If you want the map_id(s) for records with data_x = 'person' or data_y = '200' then switch the and in the where statement to or and you will receive map_id 7 & 8.
SELECT DISTINCT
b.map_id
FROM
TableA a
INNER JOIN TableB b
ON a.id = b.table_a_id
WHERE
a.data_x = 'person'
OR a.data_y = '200'
Note this encompasses (7,1)(8,2) because 1 & 2 both have data_x = 'person' and then (7,3) because 3 has data_y = '200' therefore it would return map_id 7 & 8.
select map_id from
table_b b
left outer join table_a a1 on (b.table_a_id = a1.id and a1.data_x = 'person')
left outer join table_a a2 on (b.table_a_id = a2.id and a2.data_y = '200')
group by map_id
having count(a1.id) > 0 and count(a2.id) > 0
Lets do it simple:
SELECT * FROM
(
SELECT map_id
FROM table_a a1
inner join TableB b1 ON a1.id = b1.table_a_id
where a1.data_x = 'person'
) as p
inner join
(
SELECT map_id
FROM table_a a1
inner join TableB b1 ON a1.id = b1.table_a_id
where a1.data_y = '200'
) as q
on p.map_id = q.map_id
You may replace SELECT * FROM with SELECT p.map_id FROM.
You may add more sub-set-joins to have more conditions.
sql-fiddle

(My)SQL JOIN - get teams with exactly specified members

Assume tables
team: id, title
team_user: id_team, id_user
I'd like to select teams with just and only specified members. In this example I want team(s) where the only users are those with id 1 and 5, noone else. I came up with this SQL, but it seems to be a little overkill for such simple task.
SELECT team.*, COUNT(`team_user`.id_user) AS cnt
FROM `team`
JOIN `team_user` user0 ON `user0`.id_team = `team`.id AND `user0`.id_user = 1
JOIN `team_user` user1 ON `user1`.id_team = `team`.id AND `user1`.id_user = 5
JOIN `team_user` ON `team_user`.id_team = `team`.id
GROUP BY `team`.id
HAVING cnt = 2
EDIT: Thank you all for your help. If you want to actually try your ideas, you can use example database structure and data found here: http://down.lipe.cz/team_members.sql
How about
SELECT *
FROM team t
JOIN team_user tu ON (tu.id_team = t.id)
GROUP BY t.id
HAVING (SUM(tu.id_user IN (1,5)) = 2) AND (SUM(tu.id_user NOT IN (1,5)) = 0)
I'm assuming a unique index on team_user(id_team, id_user).
You can use
SELECT
DISTINCT id,
COUNT(tu.id_user) as cnt
FROM
team t
JOIN team_user tu ON ( tu.id_team = t.id )
GROUP BY
t.id
HAVING
count(tu.user_id) = count( CASE WHEN tu.user_id = 1 or tu.user_id = 5 THEN 1 ELSE 0 END )
AND cnt = 2
Not sure why you'd need the cnt = 2 condition, the query would get only those teams where all of users having the ID of either 1 or 5
Try This
SELECT team.*, COUNT(`team_user`.id_user) AS cnt FROM `team`
JOIN `team_user` ON `team_user`.id_team = `team`.id
where `team_user`.id_user IN (1,5)
GROUP BY `team`.id
HAVING cnt = 2

MySQL: Query and join two tables

I have two tables that I believe I want to JOIN. I'm very new to this and am not completely sure…
The first table is called venues with the variables id, slug, name, etc. The second table is venue_terms with the variables id, option, venue, value. The matching variables are obviously venues.id and venue_terms.venue.
What I want to do is query venue_terms for matching values and then SELECT * FROM venues that match.
I've been working with the following query, but haven't been able to get it to work. I know INTERSECT isn't the solution, but I'm nut sure which JOIN I should use.
SELECT venue
FROM venue_terms
WHERE `option` = '1' AND `value` = '10'
INTERSECT
SELECT venue
FROM venue_terms
WHERE `option` = '2' AND `value` = '4';
I want to match those venue_terms.venue to the venues table. Can someone point me in the right direction?
UPDATE: To clarify, I'm trying to search multiple option/value combinations that ultimately have the same venue.id's. Basically, I want to able to find all of the venues where (option = 1 and value = 4) AND (option = 2 and value = 10) AND etc… where all of these are true.
You want to find venues that match conditions in two rows in table venue_terms. This can be accomplished by various methods. The most usual is by joining that table twice (another would be by a grouping query).
Here's the first way. Join twice to the venue_terms table:
SELECT v.id --- whatever columns you need
, v.slug --- from the venues table
, v.name
FROM venues AS v
INNER JOIN venue_terms AS vt1
ON vt1.venue = v.id
INNER JOIN venue_terms AS vt2
ON vt2.venue = v.id
WHERE ( vt1.option = 1 AND vt1.value = 10 )
AND ( vt2.option = 2 AND vt2.value = 4 ) ;
If you have 3 conditions, join thrice. If you have 10 conditions, join 10 times. It would be good for the efficiency of the query to have a compound index on (option, value, venue) in the terms table.
try this
SELECT venue.*, venue_terms.*
FROM venue
INNER JOIN venue_terms ON venue.id = venue_terms.venue
WHERE venue_terms.option IN ( 1 ,2)
AND venue_terms.value IN (10,4)
GROUP BY venue.id
How about this?
SELECT t1.*, t2.*
FROM venue t1 JOIN venue_terms t2
ON t1.id = t2.venue
WHERE (t2.option = 1 AND t2.value = 10)
NOTE: I believe option and value are of type INT.
If they are of type varchar then change above query to
SELECT t1.*, t2.*
FROM venue t1 JOIN venue_terms t2
ON t1.id = t2.venue
WHERE (t2.option = '1' AND t2.value = '10')
Update 1
As per your new requirement, you will just need to add that condition with OR option as shown below.
SELECT t1.*, t2.*
FROM venue t1 JOIN venue_terms t2
ON t1.id = t2.venue
WHERE
(t2.option = 1 AND t2.value = 10)
OR
(t2.option = 3 AND t2.value = 14)
This will join the two tables and print out the venues which matches the attributes (option, value) in venue_terms:
SELECT v.* FROM venue v, venue_terms vt
WHERE v.id = vt.venue
AND vt.option = 1
AND vt.value = 10

SQL - Multiple many-to-many relations filtering SELECT

These are my tables:
Cadastros (id, nome)
Convenios (id, nome)
Especialidades (id, nome)
Facilidades (id, nome)
And the join tables:
cadastros_convenios
cadastros_especialidades
cadastros_facilidades
The table I'm querying for: Cadastros
I'm using MySQL.
The system will allow the user to select multiple "Convenios", "Especialidades" and "Facilidades". Think of each of these tables as a different type of "tag". The user will be able to select multiple "tags" of each type.
What I want is to select only the results in Cadastros table that are related with ALL the "tags" from the 3 different tables provided. Please note it's not an "OR" relation. It should only return the row from Cadastros if it has a matching link table row for EVERY "tag" provided.
Here is what I have so far:
SELECT Cadastro.*, Convenio.* FROM Cadastros AS Cadastro
INNER JOIN cadastros_convenios AS CadastrosConvenio ON(Cadastro.id = CadastrosConvenio.cadastro_id)
INNER JOIN Convenios AS Convenio ON (CadastrosConvenio.convenio_id = Convenio.id AND Convenio.id IN(2,3))
INNER JOIN cadastros_especialidades AS CadastrosEspecialidade ON (Cadastro.id = CadastrosEspecialidade.cadastro_id)
INNER JOIN Especialidades AS Especialidade ON(CadastrosEspecialidade.especialidade_id = Especialidade.id AND Especialidade.id IN(1))
INNER JOIN cadastros_facilidades AS CadastrosFacilidade ON (Cadastro.id = CadastrosFacilidade.cadastro_id)
INNER JOIN Facilidades AS Facilidade ON(CadastrosFacilidade.facilidade_id = Facilidade.id AND Facilidade.id IN(1,2))
GROUP BY Cadastro.id
HAVING COUNT(*) = 5;
I'm using the HAVING clause to try to filter the results based on the number of times it shows (meaning the number of times it has been successfully "INNER JOINED"). So in every case, the count should be equal to the number of different filters I added. So if I add 3 different "tags", the count should be 3. If I add 5 different tags, the count should be 5 and so on. It works fine for a single relation (a single pair of inner joins). When I add the other 2 relations it starts to lose control.
EDIT
Here is something that I believe is working (thanks #Tomalak for pointing out the solution with sub-queries):
SELECT Cadastro.*, Convenio.*, Especialidade.*, Facilidade.* FROM Cadastros AS Cadastro
INNER JOIN cadastros_convenios AS CadastrosConvenio ON(Cadastro.id = CadastrosConvenio.cadastro_id)
INNER JOIN Convenios AS Convenio ON (CadastrosConvenio.convenio_id = Convenio.id)
INNER JOIN cadastros_especialidades AS CadastrosEspecialidade ON (Cadastro.id = CadastrosEspecialidade.cadastro_id)
INNER JOIN Especialidades AS Especialidade ON(CadastrosEspecialidade.especialidade_id = Especialidade.id)
INNER JOIN cadastros_facilidades AS CadastrosFacilidade ON (Cadastro.id = CadastrosFacilidade.cadastro_id)
INNER JOIN Facilidades AS Facilidade ON(CadastrosFacilidade.facilidade_id = Facilidade.id)
WHERE
(SELECT COUNT(*) FROM cadastros_convenios WHERE cadastro_id = Cadastro.id AND convenio_id IN(1, 2, 3)) = 3
AND
(SELECT COUNT(*) FROM cadastros_especialidades WHERE cadastro_id = Cadastro.id AND especialidade_id IN(3)) = 1
AND
(SELECT COUNT(*) FROM cadastros_facilidades WHERE cadastro_id = Cadastro.id AND facilidade_id IN(2, 3)) = 2
GROUP BY Cadastro.id
But I'm concerned about performance. It looks like these 3 sub-queries in the WHERE clause are gonna be over-executed...
Another solution
It joins subsequent tables only if the previous joins were a success (if no rows match one of the joins, the next joins are gonna be joining an empty result-set) (thanks #DRapp for this one)
SELECT STRAIGHT_JOIN
Cadastro.*
FROM
( SELECT Qualify1.cadastro_id
from
( SELECT cc1.cadastro_id
FROM cadastros_convenios cc1
WHERE cc1.convenio_id IN (1, 2, 3)
GROUP by cc1.cadastro_id
having COUNT(*) = 3 ) Qualify1
JOIN
( SELECT ce1.cadastro_id
FROM cadastros_especialidades ce1
WHERE ce1.especialidade_id IN( 3 )
GROUP by ce1.cadastro_id
having COUNT(*) = 1 ) Qualify2
ON (Qualify1.cadastro_id = Qualify2.cadastro_id)
JOIN
( SELECT cf1.cadastro_id
FROM cadastros_facilidades cf1
WHERE cf1.facilidade_id IN (2, 3)
GROUP BY cf1.cadastro_id
having COUNT(*) = 2 ) Qualify3
ON (Qualify2.cadastro_id = Qualify3.cadastro_id) ) FullSet
JOIN Cadastros AS Cadastro
ON FullSet.cadastro_id = Cadastro.id
INNER JOIN cadastros_convenios AS CC
ON (Cadastro.id = CC.cadastro_id)
INNER JOIN Convenios AS Convenio
ON (CC.convenio_id = Convenio.id)
INNER JOIN cadastros_especialidades AS CE
ON (Cadastro.id = CE.cadastro_id)
INNER JOIN Especialidades AS Especialidade
ON (CE.especialidade_id = Especialidade.id)
INNER JOIN cadastros_facilidades AS CF
ON (Cadastro.id = CF.cadastro_id)
INNER JOIN Facilidades AS Facilidade
ON (CF.facilidade_id = Facilidade.id)
GROUP BY Cadastro.id
Emphasis mine
"It should only return the row from Cadastros if it has a matching row for EVERY "tag" provided."
"where there is a matching row"-problems are easily solved with EXISTS.
EDIT After some clarification, I see that using EXISTS is not enough. Comparing the actual row counts is necessary:
SELECT
*
FROM
Cadastros c
WHERE
(SELECT COUNT(*) FROM cadastros_facilidades WHERE cadastro_id = c.id AND id IN (2,3)) = 2
AND
(SELECT COUNT(*) FROM cadastros_especialidades WHERE cadastro_id = c.id AND id IN (1)) = 1
AND
(SELECT COUNT(*) FROM cadastros_facilidades WHERE cadastro_id = c.id AND id IN (1,2)) = 2
The indexes on the link tables should be (cadastro_id, id) for this query.
Depending on the size of the tables (records), WHERE-based subqueries, running a test on every row CAN SIGNIFICANTLY hit performance. I have restructured it which MIGHT better help, but only you would be able to confirm. The premise here is to have the first table based on getting distinct IDs that meet the criteria, join THAT set to the next qualifier criteria... joined to the FINAL set. Once that has been determined, use THAT to join to your main table and its subsequent links to get the details you are expecting. You also had an overall group by by the ID which will eliminate all other nested entries as found in the support details table.
All that said, lets take a look at this scenario. Start with the table that would be EXPECTED TO HAVE THE LOWEST RESULT SET to join to the next and next. if cadastros_convenios has IDs that match all the criteria include IDs 1-100, great, we know at MOST, we'll have 100 ids.
Now, these 100 entries are immediately JOINED to the 2nd qualifying criteria... of which, say it only matches ever other... for simplicity, we are now matched on 50 of the 100.
Finally, JOIN to the 3rd qualifier based on the 50 that qualified and you get 30 entries. So, within these 3 queries you are now filtered down to 30 entries with all the qualifying criteria handled up front. NOW, join to the Cadastros and then subsequent tables for the details based ONLY on the 30 that qualified.
Since your original query would eventually TRY EVERY "ID" for the criteria, why not pre-qualify it up front with ONE query and get just those that hit, then move on.
SELECT STRAIGHT_JOIN
Cadastro.*,
Convenio.*,
Especialidade.*,
Facilidade.*
FROM
( SELECT Qualify1.cadastro_id
from
( SELECT cc1.cadastro_id
FROM cadastros_convenios cc1
WHERE cc1.convenio_id IN (1, 2, 3)
GROUP by cc1.cadastro_id
having COUNT(*) = 3 ) Qualify1
JOIN
( SELECT ce1.cadastro_id
FROM cadastros_especialidades ce1
WHERE ce1.especialidade_id IN( 3 )
GROUP by ce1.cadastro_id
having COUNT(*) = 1 ) Qualify2
ON Qualify1.cadastro_id = Qualify2.cadastro_id
JOIN
( SELECT cf1.cadastro_id
FROM cadastros_facilidades cf1
WHERE cf1.facilidade_id IN (2, 3)
GROUP BY cf1.cadastro_id
having COUNT(*) = 2 ) Qualify3
ON Qualify2.cadastro_id = Qualify3.cadastro_id ) FullSet
JOIN Cadastros AS Cadastro
ON FullSet.Cadastro_id = Cadastro.Cadastro_id
INNER JOIN cadastros_convenios AS CC
ON Cadastro.id = CC.cadastro_id
INNER JOIN Convenios AS C
ON CC.convenio_id = C.id
INNER JOIN cadastros_especialidades AS CE
ON Cadastro.id = CE.cadastro_id
INNER JOIN Especialidades AS E
ON CE.especialidade_id = E.id
INNER JOIN cadastros_facilidades AS CF
ON Cadastro.id = CF.cadastro_id
INNER JOIN Facilidades AS F
ON CF.facilidade_id = F.id