MySQL SELECT from array of ids where one is mandatory - php

I'd like to know the most efficient SQL query for achieving this problem:
Say we have a table with two columns, one storing entry ids (entry_id) and one storing category ids (cat_id):
entry_id cat_id
3 1
3 2
3 3
3 20
4 1
4 2
4 21
I'd like to count how many distinct entry_id's there are in the categories 1, 2 OR 3 but that also must be in cat_id 20.
For example, categories 1, 2 and 3 might represent music genres (Country, Pop etc.), while category 20 might be recording formats (CD, Vinyl etc.). So another way of putting it verbally could be: "How many products are there that are on Vinyl and in either the Pop or Country category?"
I could achieve this with a nested loop in code (PHP) or possibly with a nested SQL subquery, but neither feels that efficient. I feel there must be an obvious answer to this staring me in the face...
EDIT TO ADD:
I would also like to do this without modifying the database design, as it's a third party system.
FURTHER EXAMPLE TO CLARIFY:
Another real-world example of why I'd need this data:
Let's say the category ids instead represent either:
Accommodation Types (Camping = 20, Holiday Cottage = 21)
OR
Continents and their sub-regions (i.e. Europe = 1, UK = 2, England = 3)
Let's say someone has selected that they are interested in camping (cat_id = 1). Now we need to count how many camping products there are in the Europe. A product might be tagged as both Europe (parent), UK (child) AND England (grand-child), giving us an array of category ids 1, 2 or 3. So we now need to count how many distinct products there are in both those categories AND the original accommodation category of 1 (camping).
So having selected Camping, the end result might look something like:
Europe: 4 camping products
UK: 2 camping products
England : 1 camping product
Wales : 1 camping product
France: 2 camping products
etc.
Hope that helps...

I believe you want GROUP BY, COUNT() and EXISTS()
declare #t table(entry_id int, cat_id int)
insert #t select 1, 1
insert #t select 2, 1
insert #t select 1, 2
insert #t select 2, 2
insert #t select 3, 1
insert #t select 1, 20
select t1.cat_id, COUNT(*)
from #t as t1
where exists(
select * from #t
where t1.entry_id = entry_id
and cat_id = 20)
group by t1.cat_id
V2 using join instead of EXISTS()
declare #t table(entry_id int, cat_id int)
insert #t select 1, 1
insert #t select 2, 1
insert #t select 1, 2
insert #t select 2, 2
insert #t select 3, 1
insert #t select 1, 20
select t1.cat_id, COUNT(*)
from #t as t1
join #t as t2 on t1.entry_id = t2.entry_id and t2.cat_id = 20
group by t1.cat_id

select count(distinct entry_id) from myTable where cat_id=20 and entry_id in
(select distinct entry_id from myTable where cat_id in (1,2,3));

With no subqueries, using JOIN and GROUP BY:
Join the table to itself using entry_id (this gives you all possible pairs of cat_id for that entry_id). Select rows having cat_id both a member of (1,2,3) and the second cat_id = 20.
SELECT r1.entry_id
FROM records r1
JOIN records r2 USING(entry_id)
WHERE r1.cat_id IN (1,2,3)
AND r2.cat_id = 20 GROUP BY entry_id;

Related

Select rows from a table and use multiple interrelated columns for ordering

I have a table that contains information about "teams". Teams can have subteams - which has been implemented by each row having a "parent_id" column that contains the "team_id" of that subteam's parent. Here's an example of the data:
team_id team_name parent_id
1 sales (null)
2 executives (null)
3 emea sales 1
4 apac sales 1
5 uk sales 3
What I'm trying to achieve is simply to select all rows in the table in order of team_id, BUT I want subteams returned right after their parent team, also in team_id order. So, for the above example data, I would want the data returned in this order:
team_id team_name parent_id
1 sales (null)
3 emea sales 1
5 uk sales 3
4 apac sales 1
2 executives (null)
I've spent several hours wondering how to achieve this, and really haven't come up with anything useful! I would appreciate any pointers on how to approach this.
Note, I am working with an existing project and can't really make drastic database/implementation changes. I'm using PHP and Oracle with the use of stored procedures, which are new to me.
You can use a hierarchical query with the ORDER SIBLINGS BY clause, like so:
WITH teams AS (SELECT 1 team_id, 'sales' team_name, NULL parent_id FROM dual UNION ALL
SELECT 2 team_id, 'executives' team_name, NULL parent_id FROM dual UNION ALL
SELECT 3 team_id, 'emea sales' team_name, 1 parent_id FROM dual UNION ALL
SELECT 4 team_id, 'apac sales' team_name, 1 parent_id FROM dual UNION ALL
SELECT 5 team_id, 'uk sales' team_name, 3 parent_id FROM dual)
SELECT team_id,
team_name,
parent_id
FROM teams
CONNECT BY PRIOR team_id = parent_id
START WITH parent_id IS NULL
ORDER SIBLINGS BY team_id;
TEAM_ID TEAM_NAME PARENT_ID
---------- ---------- ----------
1 sales
3 emea sales 1
5 uk sales 3
4 apac sales 1
2 executives

mysql return same rows union

In my database i have categories, offers and coupons. i would like to count offers and coupons that exist in each category. when i use union it returns the same category twice.
i have the below query that returning same category rows with same name. i try to use union distinct but it does not work.
(SELECT
cat1.id AS cat1id, cat1.title AS title,
count(offers.id) AS offercounter
FROM cat1
INNER JOIN offers
ON offers.category=cat1.title
GROUP BY cat1.id
order by cat1.order)
UNION
(SELECT
cat1.id AS cat1id, cat1.title AS title,
count(coupons.id) AS couponscounter
FROM cat1
INNER JOIN coupons
ON coupons.category=cat1.title
GROUP BY cat1.id
order by cat1.order)
the result
cat1id title offercounter
2 Food 5388
23 Clothes 6000(this is offers)
32 Technology 499
40 Clothes 4(this is coupons)
i would like clothes to be (offercounter + couponscounter).
example: clothes=6004 and not two different rows
the desired result would be :
cat1id title offercounter
2 Food 5388
23 Clothes 6004(offers+coupons)
32 Technology 499
Alternative avoiding unions or sub queries is to use a couple of LEFT OUTER JOINS, and count the distinct ids from each table:-
SELECT cat1.id AS cat1id,
cat1.title AS title,
COUNT(DISTINCT offers.id) + COUNT(DISTINCT coupons.id) AS offercounter
FROM cat1
LEFT OUTER JOIN offers ON offers.category = cat1.title
LEFT OUTER JOIN coupons ON coupons.category = cat1.title
GROUP BY cat1.id AS cat1id,
cat1.title AS title
EDIT
A left outer join will return a row of nulls when there is no matching row.
For example if there was a row on cat1 with a matching row on offers but no matching row on coupons then the resulting row would consist of the row from cat1, the row from offers and the fields from coupons would be null.
This SQL will get every combination of matching rows. So if you had:-
cat1 fields offers fields coupons fields
id title id category id category
1 fred 99 fred 77 fred
1 fred 99 fred 88 fred
1 fred 100 fred 77 fred
1 fred 100 fred 88 fred
2 burt 120 fred NULL NULL
2 burt 121 fred NULL NULL
Hence the count uses DISTINCT to only could each id within a category once. As COUNT(field name) only counts non null values, with this example data for the 2nd category the count from coupons will be 0.
Union returns distinct rows. Your returned rows are distinct indeed. What you need to do to get your desired result is aggregate after unioning.
select min(cat1id) as cat1id, title, sum(offercounter) as offercounter
from
(your_query) as subquery
group by title
replace your_query with your existing query
Why don't you simple sum up the offercounter and use group by with order by cat1id.
SELECT cat1id,title,sum(offercounter) as offercounter
FROM offers GROUP BY title ORDER BY cat1id
View : SQL Fiddle
Output :
cat1id title offercounter
2 Food 5388
23 Clothes 6004
32 Technology 499
May be this can help.
SELECT cat1.id AS cat1id, cat1.title AS title ,((SELECT COUNT(offers.id) FROM offers WHERE offers.category=cat1.title)+(SELECT COUNT(coupons.id) FROM coupons WHERE coupons.category=cat1.title)) AS offercounter
FROM cat1

How to query a Sum using an array with duplicate data?

I currently have an Array in PHP with product numbers. Lets call that products.
In my database I have a table containing combinations of product numbers and the price that belongs to that product.
Problem: My Array can hold duplicate entries if for instance article #1 is ordered twice.
If I use a query like
SELECT SUM(price) FROM articles WHERE article_number IN (products)
the duplicate entry of 1 get discarded. The query I am looking for gives the sum of 10 + 10 + 12.5 + 9.95.
Is there a way to do this within MySQL?
As a clarification to my data:
products = [1, 1, 2, 3];
articles| article_number | price
__________________________________
| 1 | 10.0
| 2 | 12.5
| 3 | 9.95
Thank you :)
winmutt has a solid answer. However, if you don't have such a table to to join with then you could build your query like so:
select sum (p) from (
(select price as p from articles where article_number = 1)
union all
(select price as p from articles where article_number = 1)
union all
(select price as p from articles where article_number = 2)
union all
(select price as p from articles where article_number = 3)
) s
If your products were in a table you could do a simple JOIN:
SELECT SUM(price) FROM articles JOIN products on article_number=product_number
I thought there might be an easier way of doing this in SQL. I solved it in PHP by querying all the prices of the array's products and then in iterating over my Array in PHP to sum up the price there.

Selecting rows on Unique Pairs of columns from mysql Table

I have a Table table .
Now this has three columns table_id,content_id,content_type
What i want is to SELECT rows based on unique pairs of columns.
Say For example i have rows like this-->
id|content_id|content_type|
1 1 shirt
2 1 trouser
3 4 skirt
4 4 shirt
5 3 trouser
6 5 jeans
7 1 trouser
8 5 jeans
I want a query which selects Rows with id->1,2,3,4,5,6.
Rows with id->7,8 are not to be selected
Therefore it concludes that i dont want to select complete Duplicates of Rows
You can use a self join to pick a minimum row per group
select t.* from
test t
join (
select min(id) id ,content_id,content_type
from test
group by content_id,content_type
) t1
on(t.id = t1.id
and t.content_id = t1.content_id
and t.content_type = t1.content_type)
Demo
or if there are only these 3 columns in your table then you can simply use min()
select min(id) id ,content_id,content_type
from test
group by content_id,content_type
Demo
This is mysql-specific : If you use the GROUP BY function without using aggregate functions, the group by clause will behave as distinct, and pick up the first distinct row.
select id, content_id, content_type from test group by content_id, content_type order by id
Demo

Only return a row if result is unique

I have a db table that looks like this:
product_tag_id | tag_id | product_id
1 1 1
2 1 2
3 3 1
I have some php that's running a query in a foreach loop:
$product_tags = [1,3];
foreach(product_tags as $tag) {
$q = $this->db->query("SELECT product.*, product_tags.* FROM product
INNER JOIN product_tags ON product_tags.product_id = product.product_id
WHERE product_tags.tag_id =" . $tag);
}
The first query using 1 as the $tag value pulls in product_id 1 and 2 and the second query using 3 as $tag pulls in product_id 1 again.
I only want the query to return a row if it hasn't returned the product_id before. I only want to see a result if the product_id is unique. Is there any way to do this?
I thought DISTINCT would achieve this for me but it doesn't seem to work.
Okay, you probably need a GROUP BY. I'm not really certain what the following actually means, you make it seem as though you have some sort of date column in the table.
"I only want the query to return a row if it hasn't returned the product_id before."
I'm going to ignore this and assume that your current data is as follows and you have no additional columns to worry about:
product_tag_id | tag_id | product_id
1 1 1
2 1 2
3 3 1
but you want to return this:
product_tag_id | tag_id | product_id
1 1 1
3 3 1
i.e. you want the minimum PRODUCT_ID per tag. So, you should select that:
SELECT pt.product_tag_id, pt.tag_id, min(p.product_id) as product_id
FROM product
INNER JOIN product_tags pt
ON pt.product_id = p.product_id
WHERE pt.tag_id = ?
GROUP BY pt.product_tag_id, pt.tag_id
Including DDL in your question will always get you more and better answers.
create table products (
product_id integer primary key,
product_name varchar(15) unique
);
create table product_tags (
product_tag_id integer primary key,
tag_id integer not null,
product_id integer not null,
unique (tag_id, product_id)
);
insert into products values (1, 'a'), (2, 'b'), (3, 'c');
insert into product_tags values (1, 1, 1), (2, 1, 2), (3, 3, 1);
I only want the query to return a row if it hasn't returned the product_id before.
A different (and more useful) way to say this is "I want one row per product_id." To which we have to ask, "Then which tag_id do you want when there's more than one tag_id per product?" Here, I return the minimum tag_id.
select products.product_id, products.product_name, tags.min_tag_id
from products
inner join
(select product_id, min(tag_id) min_tag_id
from product_tags
group by product_id) tags
on tags.product_id = products.product_id
where tags.min_tag_id = 1

Categories