Proper way to join tables in mysql to have a single result - php

So I have a mysql database with three tables that has three table I am trying to retrieve rows of data from content based on a condition on data phpro_tag_types
The structures of the tables is like so
phpro_tag_types
tag_type_id | tag_type_name
<pk>
phpro_tag_targets
tag_target_id | tag_id | sub_tag_id | tag_target_name | tag_type_id
<pk> | <FK> | <FK> | | <FK>
content
content_id | tag_target_id | bunch of other things|
<pk> | <fk> |
The relationships between the table is like so
content.tag_target_id : phpro_tag_targets.tag_target_id
1 : m //each tag_target_id is in content once
//and phpro_tag_targets many times
phpro_tag_targets.tag_type_id : phpro_tag_types.tag_type_id
M: 1 // there is many occurrences of tag_type_id
//in phpro_tag_targets and one occurrence in of tag_type_id in phpro_tag_type
(I hope I have explained this thoroughly enough using the correct terms if not I apologize, clearly I am still kind of green with this)
Now I have a SQL query that looks like this
SELECT *
FROM phpro_tag_types types
INNER JOIN phpro_tag_targets targets ON types.tag_type_id=targets.tag_type_id
INNER JOIN content c ON targets.tag_target_id = c.tag_target_id
WHERE types.tag_type_id=14
ORDER BY update_time DESC
Now this query works however not exactly quite as I intended. The problem is the resulting array that is returned has multiple instance of the same piece of content ie. a single content_id (I believe because the same tag_target_id exists in phpro_tag_targets multiple times) however I would only like the results array to only contain unique content_id's as this is the data I am actually outputting to users.
As a side note putting Distinct into the query also doesn't seem to work as there is no way to only make sure content is DISTINCT (at least I could find)
Any help with this would be greatly appreciated as I am kind of lost on how to achieve this

"I am trying to retrieve rows of data from content based on a condition on data phpro_tag_types"
Assuming you are trying to fetch fields of content. What about following IN() sub-query
SELECT *
FROM content c
WHERE tag_target_id IN (
SELECT DISTINCT tag_target_id
FROM phpro_tag_types types
INNER JOIN phpro_tag_targets targets ON types.tag_type_id=targets.tag_type_id
WHERE types.tag_type_id=14
)
ORDER BY update_time DESC;
BTW,
"As a side note putting Distinct into the query also doesn't seem to work as there is no way to only make sure content is DISTINCT (at least I could find)"
DISTINCT c.* does not make sense?
SELECT DISTINCT c.*
FROM phpro_tag_types types
INNER JOIN phpro_tag_targets targets ON types.tag_type_id=targets.tag_type_id
INNER JOIN content c ON targets.tag_target_id = c.tag_target_id
WHERE types.tag_type_id=14
ORDER BY update_time DESC;

There's a general trick to do a left outer join on the duplicating table in such a way that only one entry has null values and then limit the query to that row:
SELECT *
FROM phpro_tag_types types
INNER JOIN phpro_tag_targets targets ON types.tag_type_id=targets.tag_type_id
INNER JOIN content c ON targets.tag_target_id = c.tag_target_id
LEFT OUTER JOIN phpro_tag_targets t2 on targets.tag_target_id=t2.tag_target_id
AND t2.tag_id < targets.tag_id
WHERE types.tag_type_id=1
AND t2.tag_target_id IS NULL;
This seems a bit crazy but does work and in my experience is performant unless you're dealing with a 'very large' data set (whatever that means).
I'm not 100% sure what the semantics here are, but this query is assuming that you don't care what 'tag_id' you get, you just want to get any tag that matches the 'type_id', which looks to me to match your intent.
NOTE: this will cause duplicate column names in the '*' select, so you need to limit to types.*, targets.*, content.* or spell out the column names.

Related

MySQL join query duplicates users in output

I have the following tables
ea_users
id
first_name
last_name
email
password
id_roles
ea_user_cfields
id
c_id = custom field ID
u_id = user ID
data
ea_customfields
id
name = name of custom field
description
I want to get all users which have a certain role, but I also want to retrieve all the custom fields per user. This is for the backend of my software where all the ea_users and custom fields should be shown.
I tried the following, but for each custom field, it duplicates the same user
$this->db->join('(SELECT GROUP_CONCAT(data) AS custom_data, id AS dataid, u_id, c_id
FROM ea_user_cfields userc
GROUP BY id) AS tt', 'tt.u_id = ea.id','left');
$this->db->join('(SELECT GROUP_CONCAT(name) AS custom_name, id AS customid
FROM ea_customfields AS cf
GROUP BY id) AS te', 'tt.c_id = te.customid','left');
$this->db->where('id_roles', $customers_role_id);
return $this->db->get('ea_users ea')->result_array();
the problem that u did not understand properly how join works.
its ok, that u have duplicates in select when u have relation one to many.
in few words your case: engine tries to fetch data from table "A" (ea_users) then JOIN according to the conditions another table "B" (ea_customfields). If u have one to many relation between tables (it means that one record from table "A" (lets say that we have in this table A1 record) can contain few related rows in table "B", lets call them as B1.1, B1.2 and B1.3 and B1.4), in this case it will join this records and put join result in memory. So in memory u would see something like
| FromTable A | FromTableB |
| A1 | B1.1 |
| A1 | B1.2 |
| A1 | B1.3 |
| A1 | B1.4 |
if u have 10 records in table "B", which related to the table "A" it would put 10 times in memory copy of data from table "A" during fetching. And then will render it to u.
depending on join type rows, with missing related records, can be skipped at all (INNER JOIN), or can be filled up with NULLs (LEFT JOIN or RIGHT JOIN), etc.
When u think about JOINs, try to imagine yourself, when u try to join on the paper few big tables. U would always need to mark somehow which data come from which table in order to be able to operate with it later, so its quite logically to write row "A1" from table "A" as many times as u need to fill up empty spaces when u find appropriate record in table "B". Otherwise u would have on your paper something like:
| FromTable A | FromTableB |
| A1 | B1.1 |
| | B1.2 |
| | B1.3 |
| | B1.4 |
Yes, its looks ok even when column "FromTable A" contains empty data, when u have 5-10 records and u can easily operate with it (for example u can sort it in your head - u just need to imagine what should be instead of empty space, but for it, u need to remember all the time order how did u wrote the data on the paper). But lets assume that u have 100-1000 records. if u still can sort it easily, lets make things more complicated and tell, that values in table "A" can be empty, etc, etc.. Thats why for mysql engine simpler to repeat many times data from table..
Basically, I always stick to examples when u try to imagine how would u join huge tables on paper or will try to select something from this tables and then make sorting there or something, how would u look through the tables, etc.
GROUP_CONCAT, grouping
Then, next mistake, u did not understand how GROUP_CONCAT works:
The thing is that mysqlEngine fetch on the first step structure into memory using all where conditions, evaluating subqueries + appends all joins. When structure is loaded, it tried to perform GROUPing. It means that it will select from temporary table all rows related to the "A1". Then will try to apply aggregation function to selected data. GROUP_CONCAT function means that we want to apply concatenation on selected group, thus we would see something like "B1.1, B1.2, B1.3, B1.4". Its in few words, but I hope it will help a little to understand it.
I googled table structure so u can write some queries there.
http://www.mysqltutorial.org/tryit/query/mysql-left-join/#1
and here is example how GROUP_CONCAT works, try to execute there query:
SELECT
c.customerNumber, c.customerName, GROUP_CONCAT(orderNumber) AS allOrders
FROM customers c
LEFT JOIN orders o ON (c.customerNumber = o.customerNumber)
GROUP BY 1,2
;
can compare with results with previous one.
power of GROUP in aggregation functions which u can use with it. For example, u can use "COUNT()", "MAX()", "GROUP_CONCAT()" or many many others.
or example of fetching of count (try to execute it):
SELECT c.customerName, count(*) AS ordersCount
FROM customers AS c
LEFT JOIN orders AS o ON (c.customerNumber = o.customerNumber)
GROUP BY 1
;
so my opinion:
simpler and better to solve this issue on client side or on backend, after fetching. because in term of mysql engine response with duplication in column is absolutely correct. BUT of course, u can also solve it using grouping with concatenations for example. but I have a feeling that for your task its overcomplicating of logic
PS.
"GROUP BY 1" - means that I want to group using column 1, so after selecting data into memory mySql will try to group all data using first column, better not to use this format of writing on prod. Its the same as "GROUP BY c.customerNumber".
PPS. Also I read comments like "use DISTINCT", etc.
To use DISTINCT or order functions, u need to understand how does it work, because of incorrect usage it can remove some data from your selection, (same as GROUP or INNER JOINS, etc). On the first look, you code might work fine, but it can cause bugs in logic, which is the most complicated to find out later.
Moreover DISTINCT will not help u, when u have one-to-many relation(in your particular case). U can try to execute queries:
SELECT
c.customerName, orderNumber AS nr
FROM customers c
INNER JOIN orders o ON (c.customerNumber = o.customerNumber)
WHERE c.customerName='Alpha Cognac'
;
SELECT
DISTINCT(c.customerName), orderNumber AS nr
FROM customers c
INNER JOIN orders o ON (c.customerNumber = o.customerNumber)
WHERE c.customerName='Alpha Cognac'
;
the result should be the same. Duplication in customer name column and orders numbers.
and example how to loose data with incorrect query ;):
SELECT
c.customerName, orderNumber AS nr
FROM customers c
INNER JOIN orders o ON (c.customerNumber = o.customerNumber)
WHERE c.customerName='Alpha Cognac'
GROUP BY 1
;

mySQL INNER JOIN very large tables with same column names [duplicate]

This question already has answers here:
How to resolve ambiguous column names when retrieving results?
(11 answers)
Closed 2 years ago.
I have some big tables which I need to combine into a single very large table, to form a single-page data export for a statistical package.
This is easy with INNER JOIN but the some of the tables have the same column names and these are being overwritten by each other when I fetch them as an array in PHP.
There are 4 tables being joined with 30-200 columns in each so there are far too many field names to manually include in the query with aliases, as would be the norm in this situation.
Here's the query:
SELECT * FROM logs
INNER JOIN logdetail ON logdetail.logID = logs.id
INNER JOIN clients ON clients.id = logs.clientID
INNER JOIN records ON records.id = logdetail.id
WHERE logs.userID=1
Is there any way around this? I don't actually mind what the column names are as long as I have the data so if I could prepend the table name to each field, that would do the trick.
I would create a view, your view would be comprised of your long query with aliases
Here is an example taken from the manual
mysql> CREATE TABLE t (qty INT, price INT);
mysql> INSERT INTO t VALUES(3, 50);
mysql> CREATE VIEW v AS SELECT qty, price, qty*price AS value FROM t;
mysql> SELECT * FROM v;
+------+-------+-------+
| qty | price | value |
+------+-------+-------+
| 3 | 50 | 150 |
+------+-------+-------+
This has always worked for me, unless you have one to many or some other relationship among these tables, which will duplicate records.
SELECT * FROM logs l
INNER JOIN logdetail ld ON ld.logID = l.id
INNER JOIN clients c ON c.id = l.clientID
INNER JOIN records r ON r.id = ld.id
WHERE l.userID=1
As andrew says you can also use a View to get this thing working which is much cooler.
I found a solution for this. Simply, fetch each duplicate column a second time, this time using an alias. This way, the overwritten values are selected again and aliased:
SELECT * FROM logs,
clients.name as clientName,
logs.name as logName,
etc...
INNER JOIN logdetail ON logdetail.logID = logs.id
INNER JOIN clients ON clients.id = logs.clientID
INNER JOIN records ON records.id = logdetail.id
WHERE logs.userID=1
Note: There is no need to do this for the final instance of the duplicate, because this column will not have been overwritten. So, in the example above, there is no need to include a line like records.name as recordName because, since there are no columns after it which have the same name, the record.name field was never overwritten and is already available in the name column.

Left Joining three MySQL tables

tbl_teams: team_id | team_name
tbl_players: player_id | player_fname | player_sname | player_bplace | player_bdate
tbl_players_stats: player_id | season_id | player_squad_no | team_id | player_apps | player_goals
Sorry if this is a basic question, but from all the MySQL tables and columns above I'd like to join the tables and then display the results by which season_id and team_id is selected. I need using PHP like this:
player_squad_no | player_sname, player_fname | team_name | player_apps | player_goals
I've looked at examples on here but still can't figure out how to write the MySQL query to do it with three separate tables and how to specify the table name before the column name. I've seen some examples with only the initial. tt.teams for instance. Is Left Join the way to do it?
Any help would be much appreciated.
With three separate tables, you simply write the join like this:
SELECT *
FROM Table_A AS A
LEFT JOIN Table_B AS B USING(ID)
LEFT JOIN Table_C AS C USING(ID)
Note that USING(column) is a syntactic alternative to ON A.column = B.column that you can use when the columns you want to join on have the same name in both tables.
In the above example, the tables are aliased with AS so that you can refer to them by the alias instead of the full table name. (AS is actually optional; you can just give the alias immediately after the table, if you're paying by the character.) Try to choose an alias that makes sense when you look at it; often times people will alias like this:
SELECT a.Name, b.State
FROM Customers AS a
LEFT JOIN Orders AS b
...etc.
But if you have a longer query, how are you supposed to remember what tables a and b refer to? At the very least, it would make sense to alias Customers AS C and Orders AS O; in some cases, I would go a step further: Registration AS REG, for instance. This gets more and more important as you JOIN more and more tables together.
Here's one way to write your query:
SELECT
Stats.player_squad_no,
CONCAT_WS(', ', Players.player_sname, Players.player_fname) AS player_full_name,
Teams.team_name,
Stats.player_apps,
Stats.player_goals
FROM tbl_players AS Players
LEFT JOIN tbl_players_stats AS Stats USING(player_id)
LEFT JOIN tbl_teams AS Teams USING(team_id)
The CONCAT_WS() function is included to assemble the player's full name the way you indicated you wanted it to be displayed. Since this function will output a column with a messy name, I also gave it an alias.
This should work
SELECT tbl_players_stats.player_squad_no,
tbl_players.player_sname,
tbl_players.player_fname,
tbl_teams.team_name,
tbl_players_stats.player_apps,
tbl_players_stats.player_goals
FROM tbl_players
JOIN tbl_players_stats ON tbl_players.player_id = tbl_players_stats.player_id
JOIN tbl_teams ON tbl_teams.team_id = tbl_players_stats.team_id
SELECT player_squad_no , player_sname, player_fname,team_name, player_apps, player_goals
FROM tbl_players_stats as s
JOIN tbl_players as p ON s.player_id=p.player_id
JOIN tbl_teams as t ON s.team_id=t.team_id
Nothing Joining is simple concept. But we should use proper columns for tables. While selecting the list of columns to select we should be little careful by using table aliasing. Try the below code
select c.player_squad_no,b.player_sname,b.player_fname,a.team_name,c.player_apps,c.player_goals
from tbl_teams a,tbl_players b,tbl_players_stats c
where a.team_id=c.team_id
and b.player_id=c.player_id

MySQL Combine Query and ignore duplicates

I have two tables images2 and image_data
So the goal is to have 1 table for ALL the image uploads (images2) and then the image_data is to assign that image with different options.
So for example, here is some sample data:
So image_id 10 has more than one row, because this image is associated with both TN and CQ. And they could also be associated with more than one language and slide, so I would add on to the second row and change slide_id or language_id .. if wanted to add more, than I would add a new row for it.
The goal is to have a list of all the images, and then when you click on it it pops up and you can edit the options and change it straight from there.
I need help writing a query. The one I have right now:
SELECT images2.id, images2.filename, image_data.slide_id, image_data.language_id,
image_data.type FROM images2
LEFT JOIN image_data ON images2.id = image_data.image_id
A couple things wrong with that query.. It is showing the duplicates, because image_id 10 has two rows.
But I need that second row of image #10 because I need to see that it is also associated with CQ so I can check the checkbox when it pops up.
So I need to create a query to show ALL the unique images no duplicates, with all of the options it has.
I'm not sure the best way to do this.. do I need to re-do the way my database tables are? Any help is appreciated.
Thanks.
What you could do is use GROUP_CONCAT() to turn values in multiple rows into a single concatenated string. The following retrieves the ids of slides and languages as well as their names to better facilitate your form.
SELECT
a.id,
a.filename,
GROUP_CONCAT(CONCAT(b.slide_id, '::', c.slide_name)) AS slides,
GROUP_CONCAT(CONCAT(b.language_id, '::', d.language_name)) AS languages,
GROUP_CONCAT(b.type) AS types,
FROM
images a
LEFT JOIN
image_data b ON a.id = b.image_id
LEFT JOIN
slides c ON b.slide_id = c.id
LEFT JOIN
languages d ON c.language_id = d.id
GROUP BY
a.id
Your result set for image 10 will look something like:
id | image_filename | slides | languages | types
-------------------------------------------------------------------------
10 | p170sfhe... | 5::slide5 | 1::language1 | TN,CQ
In php, just explode() the strings based on the delimiters.
you could use GROUP_CONCAT to get a csv of the fields:
SELECT
images2.id,
images2.filename,
GROUP_CONCAT(DISTINCT image_data.slide_id) AS slides,
GROUP_CONCAT(DISTINCT image_data.language_id) AS langs,
GROUP_CONCAT(DISTINCT image_data.type) AS types
FROM images2
LEFT JOIN image_data ON (images2.id = image_data.image_id)
GROUP BY images2.id

Fastest way to join to an IP2C table?

I have three tables overall, one with player names and their last login, and another table with the player name and their IP. These are from a game server, but it's two separate "plugins" of the server, so I cannot merge these into one table.
I successfully join these two on the playername column like so:
SELECT
u.`user` as `ign`,
lb.`lastlogin` as `date`,
lb.`ip`
FROM `mcmmo_users` u
LEFT JOIN `lb-players` lb
ON u.`user`=lb.`playername`
These produce the following array: Array(ign,date,ip);
However, I have an IP2C (IP-Country) table as well, and I would like to get these results at the same time. However, this table is extremely large, and would heavily slow down the query if I did a standard LEFT JOIN.
Is there a quicker way to join this? I would prefer to not query on every PHP loop of the data.
I am using MySQL and PHP
The IP2C database is layed out as follows:
begin_ip | end_ip | begin_ip_num | end_ip_num | country_code | country_name
And is queried as follows:
$IPNUM = sprintf("%u",ip2long($ip));
SELECT `country_code`
FROM `cpanel_ip2c`
WHERE `$IPNUM` BETWEEN `begin_ip_num` AND `end_ip_num`
A between condition is hard to optimize for a database. Instead, consider querying for the first IP block that is greater or equal to the user's IP:
select *
from mcmmo_users u
left join
`lb-players` lb
on u.user = lb.playername
left join
cpanel_ip2c ip
on ip.begin_ip_num =
(
select begin_ip_num
from cpanel_ip2c ip
where ip.begin_ip_num <= inet_aton(lb.ip)
order by
ip.begin_ip_num desc
limit 1
)
and inet_aton(lb.ip) <= ip.end_ip_num
With an index on cpanel_ip2c(begin_ip_num ), the country can be resolved with an index seek.
Here's an example on SQL Fiddle, with the mcmmo_users table omitted for simplicity.

Categories