I'm having a little trouble about using multiple Left Joins in a query. Some of the tables have one-to-one relationship with the left-table and some have one-to-many relation. The query looks like this:
Select
files.filename,
coalesce(count(distinct case
when dm_data.weather like '%clear%' then 1
end),
0) as clear,
coalesce(count(distinct case
when dm_data.weather like '%lightRain%' then 1
end),
0) as lightRain,
coalesce(count(case
when kc_data.type like '%bicycle%' then 1
end),
0) as bicycle,
coalesce(count(case
when kc_data.type like '%bus%' then 1
end),
0) as bus,
coalesce(count(case
when kpo_data.movement like '%walking%' then 1
end),
0) as walking,
coalesce(count(case
when kpo_data.type like '%pedestrian%' then 1
end),
0) as pedestrian
from
files
left join
dm_data ON dm_data.id = files.id
left join
kc_data ON kc_data.id = files.id
left join
kpo_data ON kpo_data.id = files.id
where
files.filename in (X, Y, Z, ........)
group by files.filename;
Here, dm_data table has a one-to-one relation with 'files' table (thats why I'm using 'Distinct'), whereas kc_data and kpo_data data has one-to-many relationship with the 'files' table. (kc_data and kpo_data can have 10 to 20 rows against one files.id). This query works fine.
The problem arises when I add another left join with another one-to-many table pd_markings (which can have 100s of rows against one files.id).
Select
files.filename,
coalesce(count(distinct case
when dm_data.weather like '%clear%' then 1
end),
0) as clear,
coalesce(count(distinct case
when dm_data.weather like '%lightRain%' then 1
end),
0) as lightRain,
coalesce(count(case
when kc_data.type like '%bicycle%' then 1
end),
0) as bicycle,
coalesce(count(case
when kc_data.type like '%bus%' then 1
end),
0) as bus,
coalesce(count(case
when kpo_data.movement like '%walking%' then 1
end),
0) as walking,
coalesce(count(case
when kpo_data.type like '%pedestrian%' then 1
end),
0) as pedestrian,
**coalesce(count(case
when pd_markings.movement like '%walking%' then 1
end),
0) as walking**
from
files
left join
dm_data ON dm_data.id = files.id
left join
kc_data ON kc_data.id = files.id
left join
kpo_data ON kpo_data.id = files.id
left join
**kpo_data ON pd_markings.id = files.id**
where
files.filename in (X, Y, Z, ........)
group by files.filename;
Now all the values become multiple of each other. Any ideas???
Note that the first two columns return 1 or 0 value. Thats the desired result actually, as one-to-one relationship tables will only have either 1 or 0 rows against any files.id, so if I don't use 'Distinct' then the resulting value is wrong (i guess because of the other tables which are returning more then one row against same file.id) No, unfortunately, my tables don't have their own unique ID columns except the 'files' table.
You need to flatten the results of your query, in order to obtain a right count.
You said you have one-to-many relationship from your files table to other table(s)
If SQL only has a keyword LOOKUP instead of cramming everything in JOIN keywords, it shall be easy to infer if the relation between table A and table B is one-to-one, using JOIN will automatically connotes one-to-many. I digress. Anyway, I should have already inferred that your files is one-to-many against dm_data; and also, the files against kc_data is one-to-many too. LEFT JOIN is another hint that the relationship between first table and second table is one-to-many; this is not definitive though, some coders just write everything with LEFT JOIN. There's nothing wrong with your LEFT JOIN in your query, but if there are multiple one-to-many tables in your query, that will surely fail, your query will produce repeating rows against other rows.
from
files
left join
dm_data ON dm_data.id = files.id
left join
kc_data ON kc_data.id = files.id
So with this knowledge that you indicate files is one-to-many against dm_data, and it is one-to-many also against kc_data. We can conclude that there's something wrong with chaining those joins and grouping them on one monolithic query.
An example if you have three tables, namely app(files), ios_app(dm_data), android_app(kc_data), and this is the data for example for ios:
test=# select * from ios_app order by app_code, date_released;
ios_app_id | app_code | date_released | price
------------+----------+---------------+--------
1 | AB | 2010-01-01 | 1.0000
3 | AB | 2010-01-03 | 3.0000
4 | AB | 2010-01-04 | 4.0000
2 | TR | 2010-01-02 | 2.0000
5 | TR | 2010-01-05 | 5.0000
(5 rows)
And this is the data for your android:
test=# select * from android_app order by app_code, date_released;
.android_app_id | app_code | date_released | price
----------------+----------+---------------+---------
1 | AB | 2010-01-06 | 6.0000
2 | AB | 2010-01-07 | 7.0000
7 | MK | 2010-01-07 | 7.0000
3 | TR | 2010-01-08 | 8.0000
4 | TR | 2010-01-09 | 9.0000
5 | TR | 2010-01-10 | 10.0000
6 | TR | 2010-01-11 | 11.0000
(7 rows)
If you merely use this query:
select x.app_code,
count(i.date_released) as ios_release_count,
count(a.date_released) as android_release_count
from app x
left join ios_app i on i.app_code = x.app_code
left join android_app a on a.app_code = x.app_code
group by x.app_code
order by x.app_code
The output will be wrong instead:
app_code | ios_release_count | android_release_count
----------+-------------------+-----------------------
AB | 6 | 6
MK | 0 | 1
PM | 0 | 0
TR | 8 | 8
(4 rows)
You can think of chained joins as cartesian product, so if you have 3 rows on first table, and has 2 rows on second table, the output will be 6
Here's the visualization, see that there is 2 repeating android AB for every ios AB. There are 3 ios AB, so what would be the count when you do COUNT(ios_app.date_released)? That will become 6; the same with COUNT(android_app.date_released), this will also be 6. Likewise there's 4 repeating android TR for every ios TR, there are are 2 TR in ios, so that would give us a count of 8.
.app_code | ios_release_date | android_release_date
----------+------------------+----------------------
AB | 2010-01-01 | 2010-01-06
AB | 2010-01-01 | 2010-01-07
AB | 2010-01-03 | 2010-01-06
AB | 2010-01-03 | 2010-01-07
AB | 2010-01-04 | 2010-01-06
AB | 2010-01-04 | 2010-01-07
MK | | 2010-01-07
PM | |
TR | 2010-01-02 | 2010-01-08
TR | 2010-01-02 | 2010-01-09
TR | 2010-01-02 | 2010-01-10
TR | 2010-01-02 | 2010-01-11
TR | 2010-01-05 | 2010-01-08
TR | 2010-01-05 | 2010-01-09
TR | 2010-01-05 | 2010-01-10
TR | 2010-01-05 | 2010-01-11
(16 rows)
So what you should do is flatten each result before you join them to other tables and queries.
If your database is capable of CTE, please use so. It's very neat and very self-documenting:
with ios_app_release_count_list as
(
select app_code, count(date_released) as ios_release_count
from ios_app
group by app_code
)
,android_release_count_list as
(
select app_code, count(date_released) as android_release_count
from android_app
group by app_code
)
select
x.app_code,
coalesce(i.ios_release_count,0) as ios_release_count,
coalesce(a.android_release_count,0) as android_release_count
from app x
left join ios_app_release_count_list i on i.app_code = x.app_code
left join android_release_count_list a on a.app_code = x.app_code
order by x.app_code;
Whereas if your database has no CTE capability yet, like MySQL, you should do this instead:
select x.app_code,
coalesce(i.ios_release_count,0) as ios_release_count,
coalesce(a.android_release_count,0) as android_release_count
from app x
left join
(
select app_code, count(date_released) as ios_release_count
from ios_app
group by app_code
) i on i.app_code = x.app_code
left join
(
select app_code, count(date_released) as android_release_count
from android_app
group by app_code
) a on a.app_code = x.app_code
order by x.app_code
That query and the CTE-style query will show the correct output:
app_code | ios_release_count | android_release_count
----------+-------------------+-----------------------
AB | 3 | 2
MK | 0 | 1
PM | 0 | 0
TR | 2 | 4
(4 rows)
Live test
Incorrect query: http://www.sqlfiddle.com/#!2/9774a/2
Correct query: http://www.sqlfiddle.com/#!2/9774a/1
I question your distinct usage here - the way it is written it will return 1 or 0. Which means a count distinct will only ever return 0, 1 or 2.
I assume you have unique ID columns in each of your tables. You can change the case to return the ID value, then count distinct that. If your join returns multiple of the same row from your pd_markings table, a distinct count on the ID will return, well, only the distinct count of rows.
Related
I count my data from database, but I have a problem with the result. the result only displays data that is not empty, while the empty data is not displayed. how do I display data rows that are empty and not empty?
the result of my query like this
pendidikan| Male | Famale | Total
----------+------+--------+------
SD | 3 | 4 | 7
SMP | 2 | 1 | 3
SMA | 1 | 3 | 4
S1 | 10 | 1 | 11
BUT i want the result like this :
pendidikan| Male | Famale | Total
----------+------+--------+------
SD | 3 | 4 | 7
SMP | 2 | 1 | 3
SMA | 1 | 3 | 4
S1 | 10 | 1 | 11
S2 | 0 | 0 | 0
S3 | 0 | 0 | 0
i want to show empty data from my database. this is my query
SELECT a.NamaStatusPendidikan, COUNT(c.IDPencaker) as total,
count(case when c.JenisKelamin='0' then 1 end) as laki,
count(case when c.JenisKelamin='1' then 1 end) as cewe
FROM msstatuspendidikan as a JOIN mspencaker as c ON
a.IDStatusPendidikan = c.IDStatusPendidikan JOIN
mspengalaman as d ON c.IDPencaker = d.IDPencaker
WHERE d.StatusPekerjaan = '0' AND c.RegisterDate
BETWEEN '2019-01-01' AND '2019-03-01' GROUP BY a.IDStatusPendidikan
Try running this query:
SELECT sp.NamaStatusPendidikan,
COUNT(*) as total,
SUM( p.JenisKelamin = 0 ) as laki,
SUM( p.JenisKelamin = 1 ) as cewe
FROM msstatuspendidikan sp LEFT JOIN
mspencaker p
ON sp.IDStatusPendidikan = p.IDStatusPendidikan AND
p.RegisterDate BETWEEN '2019-01-01' AND '2019-03-01' LEFT JOIN
mspengalaman g
ON g.IDPencaker = c.IDPencaker AND
g.StatusPekerjaan = 0
GROUP BY sp.IDStatusPendidikan;
Notes:
The JOINs have been replaced with LEFT JOINs.
Filtering conditions on all but the first table have been moved to the ON clauses.
This replaces the meaningless table aliases with table abbreviations, so the table is easier to read.
Things that looks like numbers probably are numbers, so I removed the single quotes.
This simplifies the counts, using the fact that MySQL treats booleans as numbers in a numeric context.
I am codding a search page with multiple filters and I am wondering if this is the best approach to get the results.
Each result of the search has several attributes, here I am using two attributes to simplify the example.
The main 'items' table:
id_items
1
2
The 'languages' table:
id_languages | language_code
1 es
2 en
The 'attributes_one' table:
id_attributes_one
1
2
The 'attributes_one_translations' table:
id_attributes_one_translations | id_attributes_one | id_language_code | translation
1 | 1 | 1 | Oro
2 | 1 | 2 | Gold
3 | 2 | 1 | Plata
4 | 2 | 2 | Silver
The 'attributes_one_match' table:
id_attributes_one_match | id_attributes_one | id_items
1 | 1 | 1
2 | 2 | 1
3 | 1 | 2
The 'attributes_two' table:
id_attributes_two
1
The 'attributes_two_translations' table:
id_attributes_two_translations | id_attributes_two | id_language_code | translation
1 | 1 | 2 | 99% gold
The 'attributes_two_match' table:
id_attributes_two_match | id_attributes_two | id_items
1 | 1 | 1
The concept is one item can have 0 or more match of each attribute table, and that match can have 0 or more translations.
Here is the query I am using when the user selects the filters to get all the items that have the attribute_one 'Gold' or 'Silver' order by this attribute ascendant:
SELECT
i.id_items AS id,
GROUP_CONCAT(DISTINCT aot.translation ORDER BY aot.translation DESC SEPARATOR '!¡') AS attribute_one,
GROUP_CONCAT(DISTINCT att.translation ORDER BY att.translation DESC SEPARATOR '!¡') AS attribute_two
FROM
items i
LEFT JOIN
languages AS l ON l.language_code = 'en'
LEFT JOIN
attributes_one_match AS aom ON aom.id_items = i.id_items
LEFT JOIN
attributes_one_translations AS aot ON aot.id_attributes_one = aom.id_attributes_one
AND l.id_languages = aot.id_language_code
AND (MATCH (aot.translation) AGAINST ('"Gold"' IN BOOLEAN MODE)
OR MATCH (aot.translation) AGAINST ('"Silver"' IN BOOLEAN MODE))
LEFT JOIN
attributes_one AS ao ON ao.id_attributes_one = aom.id_attributes_one
LEFT JOIN
attributes_two_match AS atm ON atm.id_items = i.id_items
LEFT JOIN
attributes_two_translations AS att ON att.id_attributes_two = atm.id_attributes_two
AND l.id_languages = att.id_language_code
LEFT JOIN
attributes_two AS at ON at.id_attributes_two = atm.id_attributes_two
GROUP BY id
ORDER BY 2 ASC
The result I get is:
id | attribute_one | attribute_two
2 | Gold | null
1 | Silver!¡Gold | 99% gold
That result is what I was expecting. Now:
* The table items will have around 300k entries once the data base is filled.
* There are 28 attributes table to match with the item.
Each attribute table will have around 20k entries, and each translation table will have 2
times the entries of the table that represents.
* Each item will have from 0 to 20 match to each item table, so I think
I wont have problems using the function GROUP_CONCAT
I am concern about the performance because the search filter page I am doing updates itself by ajax each time the user change one of the filters (it updates the filters and the results). The max results per page will be 1000 items, I didn't put the LIMIT in the query of the example.
I am not an sql expert so I don't really know if what I am doing is the best approach. I would appreciate some feedback.
Thanks a lot!
I have many forms that users fill out. Each form contains a list of questions. In this first table is the form id and the id's of the questions.
form_id | question_id
1 | 1
1 | 2
1 | 3
2 | 4
2 | 5
This table has two forms one which has 3 questions and the other 2. I have a second table which has the answers that the users have given for the questions.
user_id | form_id | question_id | answer
476 | 1 | 1 | "answer1"
476 | 1 | 3 | "answer2"
693 | 1 | 1 | "answer3"
693 | 1 | 2 | "answer4"
235 | 2 | 5 | "answer5"
In this example, 2 users have filled out form 1 and 1 user has filled in form 2. But none have filled in all the questions. Is it possible to write a query which combines the two tables and will give me the answers that the user have given including the questions that they didn't answer? I'd like the results to look like this.
user_id | form_id | question_id | answer
476 | 1 | 1 | "answer1"
476 | 1 | 2 | NULL
476 | 1 | 3 | "answer2"
693 | 1 | 1 | "answer3"
693 | 1 | 2 | "answer4"
693 | 1 | 3 | NULL
235 | 2 | 4 | NULL
235 | 2 | 5 | "answer5"
The problem that I have when I use a left join like this
select * from template t
left join answers a on a.template_id = t.template_id
AND a.question_id = t.question_id
AND t.template_id = t.template_id;
is that the row that results is missing user_id.
Yes, the specified result can be returned by a query.
One way to achieve this is a join to an inline view, and an "outer join" operation to the second table.
The "trick" is getting a distinct list of user_id and form_id from the second table, using a query, for example:
SELECT user_id, form_id
FROM second_table
GROUP BY user_id, form_id
And then using that query as an inline view (wrapping it in parens, assigning a table alias, and referencing it like it was a table in an outer query.
All that's required after that is an "outer join" to the second table.
For example:
SELECT r.user_id
, q.form_id
, q.question_id
, a.answer
FROM first_table q
JOIN ( SELECT p.user_id, p.form_id
FROM second_table p
GROUP BY p.user_id, p.form_id
) r
ON r.form_id = q.form_id
LEFT
JOIN second_table a
ON a.user_id = r.user_id
AND a.form_id = r.form_id
AND a.question_id = q.question_id
ORDER
BY r.user_id
, q.form_id
, q.question_id
Note that the keyword "LEFT" specifies an outer join operation, returning all rows from the left side, along with matching rows from the right side. A typical "inner" join would exclude rows that didn't find a matching row from the table on the right side.
use
left join
something like:
select * from table1 left join table2 on table1.form_id= table2.form_id
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)
I'm trying to use the SUM function to count rows from 3 tables, which is however, not working effectively since when the total_files and total_notes are returned, they both are the same when there is at least one file and then total_files will take the same value as total_notes which I don't understand why it's doing that.
It should count the number of rows which is relevant to each record that will get return as a record list with a count of total files, total notes and total contacts assigned to the record per record row (the data of files, notes and contacts do not get displayed only counted).
My query is shown below:
SELECT rec.street_number,
rec.street_name,
rec.city,
rec.state,
rec.country,
rec.latitude,
rec.longitude,
LEFT(rec.description, 250) AS description,
usr.username,
usr.full_name,
ppl.person_id,
ppl.first_name,
ppl.last_name,
SUM(IF(rlk.record_id = rec.record_id, 1, 0)) AS total_contacts,
SUM(IF(files.record_id = rec.record_id, 1, 0)) AS total_files,
SUM(IF(notes.record_id = rec.record_id, 1, 0)) AS total_notes,
(
SELECT COUNT(DISTINCT rec.record_id)
FROM records rec
WHERE rec.marked_delete = 0 AND rec.is_archive = 0
) AS total_records
FROM
(
records rec
INNER JOIN members usr ON rec.user_id = usr.user_id
LEFT OUTER JOIN record_links rlk ON rec.record_id = rlk.record_id
LEFT OUTER JOIN people ppl ON ppl.person_id = rlk.person_id AND rlk.record_id = rec.record_id
LEFT OUTER JOIN files files ON files.record_id = rec.record_id
LEFT OUTER JOIN notes notes ON notes.record_id = rec.record_id
)
WHERE rec.marked_delete = 0 AND rec.is_archive = 0
GROUP BY rec.record_id
ORDER BY rec.submit_date DESC
LIMIT 0, 25
Basically as you can see there is three SUM which will count relevant rows that comes from those tables, but I seriously don't understand how total_files would be taking the same value as total_notes is there something wrong I'm doing here?
It's because rec is joined to both notes and files.
Suppose record 1 has 2 notes and 1 file, record 2 has two note and two files, and record 3 has a note but no files.
Then the table rec LEFT OUTER JOIN files ... LEFT OUTER JOIN notes will look like this:
+-----------+---------+---------+
| record_id | file_id | note_id |
+-----------+---------+---------+
| 1 | 1 | 1 |
| 1 | 2 | 1 |
| 2 | 3 | 2 |
| 2 | 3 | 3 |
| 2 | 4 | 2 |
| 2 | 4 | 3 |
| 3 | NULL | 4 |
+-----------+---------+---------+
Note how every file_id gets joined to every note_id (within the same record_id). Also, since you have SUM(IF(files.record_id = rec.record_id,1,0)) and the join condition is files.record_id = rec.record_id, you are actually counting COUNT(files)*COUNT(notes) per record_id.
I'd recommend you instead COUNT(DISTINCT files.id) and COUNT(DISTINCT records.id). The column in the COUNT would be your primary key on files/notes, not files.record_id:
SELECT rec.record_id,
COUNT(DISTINCT files.id) AS total_files,
COUNT(DISTINCT notes.id) AS total_notes
FROM rec
-- note: LEFT OUTER JOIN is the same as LEFT JOIN in MySQL
LEFT JOIN files ON files.record_id=rec.record_id
LEFT JOIN notes ON notes.record_id=rec.record_id
GROUP BY record_id
+-----------+-------------+-------------+
| record_id | total_files | total_notes |
+-----------+-------------+-------------+
| 1 | 2 | 1 |
| 2 | 2 | 2 |
| 3 | 0 | 1 |
+-----------+-------------+-------------+
Of course, adjust to your query as necessary (add in those extra columns/joins).