Database setup:
http://sqlfiddle.com/#!2/4d1c2/1
Following query selects all tags which belongs to productID and their places, comma separated:
SELECT CONCAT_WS(',', GROUP_CONCAT(Tags.Name))
FROM `ProductTags`
LEFT JOIN Tags ON ProductTags.TagID = Tags.TagID
WHERE `ProductID` = 46356
GROUP BY DisplayOrder
It can contain 1-3 rows.
More complex query shows category, full of (like 50-100) products.
I want all tags be available at once, pass them to juery and then display.
The question is: how can i concat() this query into one field, so i only have one big query, or should i handle it with php and have like 100 queries at page?
I donĀ“t know if I get you right, but this could be one solution:
SELECT CONCAT_WS(',', GROUP_CONCAT( DISTINCT Tags.Name))
FROM `ProductTags`
LEFT JOIN Tags ON ProductTags.TagID = Tags.TagID
This will show you all Tags for all productTags. (DISTINCT makes the names unique)
Related
Let's say you have
records table with id and name
tags table with id and name
records_tags with record_id and tags_id (relationship table)
Now you want to run a query to include records that have X tags and exclude records that have X tags.
You could do INNER JOIN, but the challenge here is, when there are many tags to a record, it creates duplicates within the results.
Example:
inner join `records_tags` on `records_tags`.`record_id` = `records`.`id`
and `records_tags`.`tag_id` in (?) and `records_tags`.`tag_id` not in (?)
As for the Laravel side, Ive used:
$records->join('records_tags', function ($join) use($include, $exclude) {
$join->on('records_tags.record_id','=','records.id');
if ($include) $join->whereIn('records_tags.tag_id',$include);
if ($exclude) $join->whereNotIn('records_tags.tag_id',$exclude);
});
Could there be a better solution to handle this or a way to ask for it to create unique or distinct rows, the goal of the join is only to include or exclude the actual records themselves from the results?
Edit:
The only other thing I can think of is doing something like this, still have to run tests to see accuracy, but for a crude solution
Edit 2: This doesn't appear to work on NOT IN as it creates duplicates.
$records->join(\DB::raw('(SELECT tag_id, record_id FROM records_tags WHERE records_tags.tag_id IN ('.implode(',',$include).'))'),'records_tags.record_id','=','records.id');
The conditions in the ON clause:
... and `records_tags`.`tag_id` in (?) and `records_tags`.`tag_id` not in (?)
do not exclude from the results the ids of the records that you want to exclude.
Any id that is linked to any of the wanted tags will be returned even if it is also linked to an unwanted tag, because the joins return 1 row for each of the linked tags.
What you can use is aggregation and the conditions in the HAVING clause:
SELECT r.id, r.name
FROM records r INNER JOIN records_tags rt
ON rt.record_id = r.id
GROUP BY r.id -- I assume that id is the primary key of records
HAVING SUM(rt.tag_id IN (?)) > 0
AND SUM(rt.tag_id IN (?)) = 0;
or, if you want the ids that are linked to all the wanted tags, use GROUP_CONCAT():
SELECT r.id, r.name
FROM records r INNER JOIN records_tags rt
ON rt.record_id = r.id
GROUP BY r.id
HAVING GROUP_CONCAT(rt.tag_id ORDER BY rt.tag_id) = ?;
In this case you will have to provide for the wanted tags ? placeholder a sorted comma separated list of ids.
For my college project I am doing a model e-commerce website for learning. I have searched for similar questions, however I am unable to get deeper ideas.
My current tables structure:
Products - sku(PK), name, price, other_columns
ProductMedia - path(UNIQUE), product_sku (from products table).
The path contains the filename of the media.
When I retrieve a product data, I select the columns from table: products and LEFT JOIN productMedia and GROUP_CONCAT and get the paths.
Query:
SELECT
p.*, // for simplicity mentioned as *
(GROUP_CONCAT(path SEPARATOR ',') FROM productMedia WHERE product_sku = p.sku) as product_images
FROM products p
WHERE SOME_CONDITION // Single or multiple conditions associated with table products alone
To display, I explode the product_images and loop to display or use it for other purposes.
Now my questions are:
Is this efficient approach? Or any better approach which I am unaware of?
When multiple columns need to be concatenated from the child table (here productMedia), I am writing GROUP_CONCAT (column SEPARATOR) as concatenatedColumn multiple times, will this not affect the performance?
Some production example queries,
SELECT
p.name, p.sku, p.mrp, p.selling_price,
(GROUP_CONCAT(path SEPARATOR ',') FROM productMedia WHERE product_sku = p.sku) as product_images
FROM products p
WHERE p.category = 'books'
SELECT
p.name, p.sku, p.mrp, p.selling_price,
(GROUP_CONCAT(path SEPARATOR ',') FROM productMedia WHERE product_sku = p.sku) as product_images
FROM products p
WHERE p.selling_price < 1000 AND p.category = 'toys'
I try not to use GROUP_CONCAT() to fetch child rows like the scenario you describe.
I would do this simpler query:
SELECT p.*, m.* // really write out the columns, don't use *
FROM products p
LEFT OUTER JOIN productMedia m ON m.product_sku = p.sk
WHERE <conditions on p>
This query is easier to write and easier to optimize.
It does return a variable number of rows for each p, because you may have 0, 1, or many matching rows in productMedia. The values of p.* will be duplicated for every matching row of m. It's not hard to process that in your application code as you fetch the rows.
Formally (dependent by SOME_CONDITION this conditions must be placed to WHERE, HAVING or both - so I cannot insert such a generic placeholder into the query without conditions clarification):
SELECT p.*,
GROUP_CONCAT(pm.path SEPARATOR ',') paths
FROM Products p
LEFT JOIN ProductMedia pm ON p.sku = pm.product_sku
GROUP BY p.sku;
If ONLY_FULL_GROUP_BY is enabled you'll get an error. This query is safe for partial grouping, so I'd recommend to alter session SQL Mode for this query (all 3 queries must be executed in the same connection strongly! the best way is to execute them in one multi-query statement):
SET ##sql_mode := REPLACE((#old_sql_mode := ##sql_mode), 'ONLY_FULL_GROUP_BY', '');
SELECT p.*,
GROUP_CONCAT(pm.path SEPARATOR ',') paths
FROM Products p
LEFT JOIN ProductMedia pm ON p.sku = pm.product_sku
GROUP BY p.sku;
SET ##sql_mode := #old_sql_mode;
If there is no matched record in ProductMedia you'll get NULL for such product. If NULL is not safe for you, use COALESCE(GROUP_CONCAT(pm.path SEPARATOR ','), '') paths instead.
Is this efficient approach? Or any better approach which I am unaware of?
Yes, it is efficient enough. Of course, you may select one path per record (and obtain a lot of p.* fields values duplicates) - this will slightly decrease the amount of work for PHP but increase data transter and memory usage size. Additionally in my approach you do not need in additional output recordset pre-sorting by p.sku.
When multiple columns need to be concatenated from the child table (here productMedia), I am writing GROUP_CONCAT(column SEPARATOR) as concatenatedColumn multiple times
You may use both separate GROUP_CONCAT for each field, or concatenate everything into one value, like
GROUP_CONCAT(CONCAT_WS(',', field1, field2,...) SEPARATOR ',')
You may use both the same separator (then explode and iterate over one array) and different separators (and explode by records, then explode each record value by fields).
will this not affect the performance?
But you need in this data? so no way, you must do...
And "the difference in performance" - while compare with what?
I have 2 following tables in my DB:
Statements
and Tags
When I need to select all the statements matching some tag ($tag), I use this SQL query:
$select = "SELECT Statements.id, Statements.name, Statements.link FROM Statements JOIN Tags ON Statements.id = Tags.probID AND Tags.tag = '$tag'";
Now I need to select all the statements matching a few tags in the same time. How can I do that? Should I change something in my DB?
You can match the tags using IN and then use GROUP BY to ensure that you have all of them:
SELECT s.id, s.name, s.link
FROM Statements s JOIN
Tags t
ON s.id = t.probID
WHERE t.tag IN ('$tag1', '$tag2', '$tag3')
GROUP BY s.id, s.name, s.link
HAVING COUNT(DISTINCT t.tag) = 3;
Note: the above example is for three tags. The number in the HAVING clause needs to match the number of tags you are looking for.
Gordon's answer is good. Nonetheless I want to give you an advice here: Don't make it a habit to join everything first and only then think how to get to your data.
Your original problem was simply to find statements for which exist a certain tag, or in other words that has a tag in the other table. Hence use EXISTS or IN.
select id, name, link
from statement
where id in (select probid from tags where tag = #tag);
You prabably see that this is more straight-forward than your query, because it is written like one might word the task: select data from statement where the ID is in the set of IDs tagged with #tag.
As to the additional requirement, Gordon's way is perfect: Look at the IDs in tags (i.e. group by the ID) and find the one having all tags. That changes above statement to:
select id, name, link
from statement
where id in
(
select probid
from tags
where tag in (#tag1, #tag2, #tag3)
group by probid
having count(distinct tag) = 3
);
I have a query that returns results related to items that match a specific category...
There are 3 mysql tables that results to this, items, categories and item_categories.
These i assume are self explanatory, but the latter, is a linking table that links any specific item to any specific category, using a match of id's.
The items table contains one row, with an id value of 1.
The categories table is filled with 15 rows, with id values of 1-15.
the item_categories table contains one row, the item_id value is 1 and the category_id value is 5.
This is the mysql query in its php form:
$catResultQuery = "
SELECT i.id, name, price
FROM items i
INNER JOIN item_categories
ON i.id = item_id
INNER JOIN categories c
ON category_id = c.id
WHERE MATCH (c.id)
AGAINST ('{$_SESSION['input']}' IN BOOLEAN MODE)
ORDER BY name
";
The session variable has a value of 5, but for some reason, this query displays a 0 result set.
Even when i run the query in php myadmin, it returns 0 rows.
And i am confused, because in my head, the logic behind all of this seems fairly simple, but for some reason i get 0? Does anyone have any idea where i have gone wrong with this?
Any advice and input would be greatly appreciated, thank you!!
Ok, I see now that you're building the SQL dynamically. If that's the case, then this should work:
SELECT i.id, name, price
FROM items i
INNER JOIN item_categories
ON i.id = item_id
INNER JOIN categories c
ON category_id = c.id
WHERE c.id
IN ('{$_SESSION['input']}')
ORDER BY name
Just make sure '{$_SESSION['input']}' is comma delimited and be aware that this carries the risk of SQL injection because you're constructing the SQL on the fly.
I am first doing a search on the table tags which results in all the rows returned together with their additional data from the table links (on which i do a join). The table tags works such that for an auto _id there can be multiple rows (1 tag per row so multiple tags is multiple rows).
What I want to do is that instead of just getting multiple rows back which match the auto _id and query. I want to get each tag back for each auto_id that is found, in 1 row (some form of distinct but that puts all the tags comma seperated in 1 field), hence the group_concat (which obviously is not working at the moment - i've added it as pseudo).
SELECT ta.auto_id, li.address, li.title, GROUP_CONCAT(SELECT tag FROM tags WHERE auto_id = ta.auto_id)
FROM `tags` AS ta
JOIN
links AS li
ON ta.auto_id = li.auto_id
GROUP BY ta.tag
WHERE ta.user_id = 1
AND (ta.tag LIKE '%query%')
I hope I've made my case clear.
Thank you very much,
Ice
If I understand what you're asking, something like this should do the trick:
SELECT ta.auto_id, li.address, li.title, GROUP_CONCAT(ta.tag) -- Get a comma separated list of all the tags
FROM tags AS ta
INNER JOIN links AS li ON ta.auto_id = li.auto_id
WHERE ta.user_id = 1
AND ta.tag LIKE '%query%'
GROUP BY li.id -- Group by links id, so you get one row for each link, which contians a list of tags