MySQL Select entry with no associated entry(in another table) - php

I am making an custom online management application for a company where users can manage clients, products, components, providers, etc. Right now I am working on a search form, where users can search for clients using keywords and various options. Now, for the keywords search, I know how to use MySQL FullText, the problem is more related to the extra search options.
So basically, every client can be assigned to one or more categories. A client might also no be assigned to any category. To do so, I use three tables:
"clients" table, which holds the basic information of the clients, such as name, contact info, etc. Each clients has a unique ID
"categories" table, which simply has ID, title and description fields
"categories_assign" table, which has only two fields, "clientId" and "categoryId". It allows me to assign multiple category to a client.
Now, in the search form, the user is able to select categories to search in; multiple categories can be selected at once, as well as a "None" value, which should search clients without category. A client without any category will basically not have any entry in the "categories_assign" table.
That's my problem: I have no problem searching for clients assigned to specific categories, but I can't find a way to search clients Without category..
Here's a simplified version (for readability purposes) of the query that select clients according to the selected categories:
SELECT * FROM clients c, categories_assign a WHERE c.id = a.clientId AND a.categoryId IN(1,7,43,52)
So as you might expect, this select clients which is assigned to categories ID 1, 7, 43 or 52. This does works fine. However, as I stated before, I can't find a way to select clients without categories, ie. clients that do not have any entry in the "categories_assign" table. I want to be able to select specific categories too, at the same time. (for example, search for categories ID 1, 7, 43, 52 AND clients without category). I tried using joins, but without luck.
The only workaround I can think of is to create an entry in the "categories_assign" table with "categoryId" set to 0, meaning "no category", so i'd simply need to search for "categoryId" 0. I'd like to avoid the need of doing this, if possible.
Do anyone has any clues?
Thanks in advance!

SELECT DISTINCT c.*
FROM clients c
LEFT JOIN categories_assign a ON c.id = a.clientId
WHERE a.id IS NULL
OR a.categoryId IN (1, 7, 43, 52)
The a.id IS NULL gets those with no category assignments. The a.categoryId IN (...) gets those assigned to those categories.
If you're writing a query solely to get the uncategorized records, you don't need the DISTINCT clause. That's just there to eliminate duplicate records arising from the possibility that a client may be assigned to multiple categories you're looking for.

SELECT * FROM clients c
WHERE c.id NOT IN (SELECT DISTINCT a.clientID FROM categories_assign a)
This will return all clients who do not appear in the categories_assign table. If you want to also do a search for clients with categories at the same time, just UNION this query and your query.

Related

Advanced webshop filtering w/ MySQL

I am making a webshop in MySQL and PHP with a database design similar to this (see the picture)
I am not having a problem with multiple-price items with different specifications, like in the question linked above.
This will be a computer webshop so there is no need for me to make a specification for each product. I can categorize them, for ex. Smartphones, Laptops... and add specifications for each category.
Smartphones could have: Brand, Processor type, Screen Size, Color, Battery, Price, etc.
Laptops: Processor, Graphics card, Brand, Price, Size, ...
I also see no need for a [combinations] table, as in my opinion, it would be much to store needlessly.
My problem is with filtering the products. I can easily select all of them, or just a single one, or for example. select all of the white colored phones or all of the white or gold colored ones, but I cannot select all of the white phones that have an 8MP front camera.
Table structures:
[products]: id, name, category_id, description
[categories]: id, name
[specifications]: id, category_id, name
[spec_values]: id, product_id, spec_id, value
The following SQL returns all of the distinct products that are golden or have an 8mp front camera.
SELECT products.id, products.name, spec_values.value AS Filter
FROM products, categories, specifications, `spec_values`
WHERE products.category_id = categories.id
AND spec_values.product_id = products.id
AND ((specifications.name = 'Color' AND spec_values.value = 'Gold')
OR (specifications.name = 'Front Camera' AND spec_values.value = '8MP'))
GROUP BY products.id;
I want to have a logical AND instead an OR between them but simply changing it returns nothing, as it cannot be fulfilled within a single row because my specification names and values are stored on multiple rows.
One solution would be to SQL CREATE TEMPORARY TABLE for each filter, but I think that would require a lot of unnecessary server-side work.
The other one would be to get all of the non-distinct values and iterate through them with PHP and check if all of them exist; if yes then display them on the page, but still, that one could be messy, too.
Is there a better solution that I did not notice due to my inexperience in SQL?
Thank you in advance for any help. Have a great day!
You can use aggregation for this. Include a HAVING clause to check, that the count matches the number of filters. Unless you have inserted the exact specification value twice (or more) you'll know that the products meeting that condition must have all the specification values filtered for. If one does not, one row (or more rows), i.e. one (or more) specificion value, is missing.
SELECT products.id,
products.name
FROM products
INNER JOIN spec_values
ON spec_values.product_id = products.id
INNER JOIN specifications
ON specifications.id = spec_values.spec_id
WHERE specifications.name = 'Color' AND spec_values.value = 'Gold'
OR specifications.name = 'Front Camera' AND spec_values.value = '8MP'
GROUP BY products.id,
products.name
HAVING count(*) = 2;
BTW, it is advisable to use explicit join syntax over the comma delimited list in the FROM clause for better readability.

Showing users who liked an item in an item list

This is an issue that I've deemed impractical to implement but I would like to get some feedback to confirm.
I have a product and users database, where users can like products, the like data is stored in a reference table with just pid and uid.
The client request is to show 3 users who have liked every product in the product listing.
The problem is, its not possible to get this data in one query for the product listing,
How I once implemented and subsequently un-implemented it was to perform a request for the users who have liked the products during the loop through the product list.
ie.
foreach($prods as $row):
$likers = $this->model->get_likers($row->id);
endforeach;
That works, but obviously results in not only super slow product listings, and also creates a big strain on the database/cpu.
The final solution that was implemented was to only show the latest user who has liked it (this can be gotten from a join in the products list query) and have a link showing how many people have liked, and upon clicking on it, opens a ajax list of likers.
So my question is, is there actually a technique to show likers on the product list, or is it simply not possible to execute practically? I notice actually for most social media sites, they do not show all likers on the listings, and do employ the 'click to see likers' method. However, they do show comments per items on the listing, and this is actually involves the same problem doesn't it?
Edit: mock up attached on the desired outcome. there would be 30 products per page.
By reading your comment reply to Alex.Ritna ,yes you can get the x no. of results with per group ,using GROUP_CONCAT() and the SUBSTRING_INDEX() it will show the likers seperated by comma or whatever separator you specified in the query (i have used ||).ORDER BY clause can be used in group_concat function.As there is no schema information is available so i assume you have one product table one user table and a junction table that maintains the relation of user and product.In the substring function i have used x=3
SELECT p.*,
COUNT(*) total_likes,
SUBSTRING_INDEX(
GROUP_CONCAT( CONCAT(u.firstname,' ',u.lastname) ORDER BY some_column DESC SEPARATOR '||'),
'||',3) x_no_of_likers
FROM product p
LEFT JOIN junction_table jt ON(p.id=jt.product_id)
INNER JOIN users u ON(u.id=jt.user_id)
GROUP BY p.id
Fiddle
Now at your application level you just have to loop through the products and split the x_no_of_likers by separator you the likers per product
foreach($prods as $row):
$likers=explode('||',$row['x_no_of_likers']);
$total_likes= $row['total_likes'];
foreach($likers as $user):
....
endforeach;
endforeach;
Note there is a default 1024 character limit set on GROUP_CONCAT() but you can also increase it by following the GROUP_CONCAT() manual
Edit from comments This is another way how to get n results per group, from this you can get all the fields from your user table i have used some variables to get the rank for product group ,used subquery for junction_table to get the rank and in outer select i have filtered records with this rank using HAVING jt.user_rank <=3 so it will give three users records per product ,i have also used subquery for products (SELECT * FROM product LIMIT 30 ) so the first 30 groups will have 3 results for each,for below query limit cannot be used at the end so i have used in the subquery
SELECT p.id,p.title,u.firstname,u.lastname,u.thumbnail,jt.user_rank
FROM
(SELECT * FROM `product` LIMIT 30 ) p
LEFT JOIN
( SELECT j.*,
#current_rank:= CASE WHEN #current_rank = product_id THEN #user_rank:=#user_rank +1 ELSE #user_rank:=1 END user_rank,
#current_rank:=product_id
FROM `junction_table` j ,
(SELECT #user_rank:=0,#current_rank:=0) r
ORDER BY product_id
) jt ON(jt.product_id = p.id)
LEFT JOIN `users` u ON (jt.`user_id` = u.`id`)
HAVING jt.user_rank <=3
ORDER BY p.id
Fiddle n results per group
You should be able to get a list of all users that have liked all products with this sql.
select uid,
count(pid) as liked_products
from product_user
group by uid
having liked_products = (select count(1) from products);
But as data grows this query gets slow. Better then to maintain a table with like counts that is maintained through a trigger or separately. On every like/dislike the counter is updated. This makes it easy to show the number of likes for each product. Then if the actual users that liked that product is wanted do a separate call (on user interaction) that fetches the specific likes for one product). Don't do this for all products on a page until actually requested.
I am assuming the size of both these tables is non-trivially large. You should create a new table (say LastThreeLikes), where the columns would be pid,uid_1,uid_2 and uid_3, indexed by pid. Also, add a column to your product table called numLikes.
For each "like" that you enter into your reference table, create a trigger that also populates this LastThreeLikes table if the numLikes is less than 3. You can choose to randomly update one of the values anyway if you want to show new users once in a while.
While displaying a product, simply fetch the uids from this table and display them back.
Note that you also need to maintain a trigger for the "Unlike" action (if there is any) to re-populate the LastThreeLikes table with a new user id.
Problem
The problem is the volume of data. From the point of view that you need two integer value as a answer you should forget about building a heavy query from your n<->n relations table.
Solution
Generates a storable representation using the file_put_contents() with append option each time a user likes a product. I don't have enough room to write the class in here.
public function export($file);
3D array format
array[product][line][user]
Example:
$likes[1293][1][456]=1;
$likes[82][2][656]=1;
$likes[65][3][456]=1;
.
.
.
Number of users who like this particular product:
$number_users_like_this_product = count($likes[$idProduct]);
All idUser who like this particular product:
$users_like_this_product = count($likes[$idProduct][$n]);
All likes
$all_likes = count($likes);
Deleting a like
This loop will unset the only line where $idProduct and $IdUser you want. Since all the variables are unsigned integer it is very fast.
for($n=1, $n <= count($likes[$idProduct]), $n++)
{
unset($likes[$idProduct][$n][$idUser]);
}
Conclusion
Get all likes will be easy as:
include('likes.php');
P.S If you want to give a try i will be glad to optimize my stuff and share it. I've created the class in 2012.

How do you find and list stable roommate matches with SQL or PHP?

Prerequisites
I have two tables. A list of people in one table, and how they prefer each other in a foreign key lookup table. The first table is only the list of people. The other is where they all list a few other people they would prefer to have as a roommate.
Table People:
List of people with ID, name and surname, etc
Table Choices:
List of choosers (FK People ID)
List of chosen ones (FK People ID)
Question
How can I list matches with SQL (or PHP)? That is, where one person is also on the list on the person he wanted to have as a roommate? Basically you have a chooser with a list of chosen ones. How would you check if the chooser is also on the list of one of his or her chosen ones?
Basically I want a report with every stable match, that is where the chooser is also on the list of at least one of his or her chosen ones.
I am guessing a for loop would do the trick, but how would you even put together the first iteration? Much less the rest of the loop?
Join based solution:
SELECT
r1.name as name1,
r2.name as name2
FROM
roommate r1
JOIN
roommate_pair rp1 ON r1.id = rp1.chooser_id
JOIN
roommate r2 ON r2.id = rp1.choosen_id
JOIN
roommate_pair rp2 ON r2.id = rp2.chooser_id
WHERE
rp2.choosen_id = r1.id
GROUP BY
CONCAT(GREATEST(r1.id,r2.id),'-',LEAST(r1.id,r2.id))
Last GROUP BY is to remove duplicate matches in swapped columns. Working SQL Fiddle
SELECT a.chooser, a.chosen
FROM roommates a,roommates b
WHERE a.chooser = b.chosen
AND a.chosen = b.chooser;
Using the above query you should get the cross-referenced id's... You do, however, get doubles (both references are returned). See SQL Fiddle.
You could do a check on that in your PHP-code.
This piece of code should provide you some hint. First you will iterate through all the people. Then from the list of possible preferred people, you select only those who, in turn, have the original person in their list of preferred people.
for cc in (select * from people) loop
for dd in (select * from preferences pr where pr.source_id = cc.people_id and exists (select 1 from preferences pr1 where pr1.source_id = pr.friend_id and pr1.friend_id = cc.people_id)) loop
--do your stuff here
end loop
end loop

Order results from one MySQL table by another, which is saving multiple values into the order field

I'm trying to solve a MySQL problem, I have two tables:
CATEGORIES (COLUMNS: _id, _name)
POSTS (COLUMNS: _id, _category, _title, _text)
_category field in POSTS is LONGTEXT and can have multiple CATEGORIES _ID's separated only by , using implode(",") PHP function.
I try to list with PHP the 10 most popular categories, and to display in () the posts in them, but without luck.
I'm not very familar with MySQL, I only know how to use SELECT FROM WHERE ORDER LIMIT, INSERT & UPDATE so I will be very happy if someone can give me a good solution. I tried to use IN() but IN() needs the _category field of POSTS to be like this '1','2','3','4', now its 1,2,3,4 without the quotes, so if anyone know how I can transform this field into list without FIELD TYPE SET, I will be pretty happy.
You may want to change your relation model to the following:
Table CATEGORIES with columns:
_id
_name
Table POSTS with columns:
_id
_title
_text
Table POSSESS with columns:
post_id (FOREIGN KEY)
category_id (FOREIGN KEY)
A tuple in POSSESS relation (table) means the post_id is in the category_id category.
the key word for this is "many-to-many" relations, if possible refactor your scheme like Mark Baker wrote.
Using the model that Dyin suggested, you would then use something like this to list the top 10 categories by popularity (assuming that the more posts a category has, the more popular it is):
SELECT
c.*, # get all values for rows in categories
count(p.post_id) AS post_count # here we are counting the posts for each category using a field alias for the count
FROM (
categories AS c, # we are aliasing the tables also to shorten the typing a bit
possess AS p # you could also use aliases to join the same table multiple times
)
WHERE
c.id = p.category_id # link the categories and the possess tables
GROUP BY c.id # without this, the query would just count all posts, this way the result set is separated into groups by category
ORDER BY post_count DESC
LIMIT 10
Given what you said about your experience with SQL, this query might seem a bit over the top for now, but I think you could use as a starting point for learning more, as always, google is your friend. Start by researching how to link tables using foreign keys and joins.
I've used this:
SELECT *, (SELECT COUNT(*) FROM offer WHERE FIND_IN_SET(type._id, offer._type)) AS _count FROM type ORDER BY _count DESC LIMIT 0, 10
Works fine for now, its table type (columns: _id, _name) and offer (columns: .., .., _types,

multiple criteria search

I have multiple tables with Customer data (ex Customer Name, Customer Contact Name, Customer Service Item etc).
I need to enable search on these multiple columns across tables. Then I have to use the search result to pull Customer information (I need Customer ID, or Customer Name).
What is the best way to do this?
Possible Solutions:
Offer multiple filters (different search boxes), and then handle each result separately. (The client does not prefer this, and wants it in a single box.)
Create a temp table (CustomerID, Search Field Values).
Create index !?
User inner join, and put logic into handling the search result!!!!
Thanks.
try something like:
SELECT
c.*
FROM CustomerTable c
INNER JOIN (SELECT
CustomerID
FROM Table1
WHERE columnA =filter1
UNION
SELECT
CustomerID
FROM Table2
WHERE columnB =filter2
UNION
SELECT
CustomerID
FROM Table3
WHERE columnC =filter3
) dt ON c.CustomerID=dt.CustomerID
http://dev.mysql.com/doc/refman/5.1/en/fulltext-search.html
I do believe this similar to Oracle Text Search etc which is used in Oracle applications to allow more intelligent searches, "google-likish".
So it is a fulltext index which is to be created.
Doing it with inner joins (or worse, copying stuff around in temporary tables) might work but the code will be complex and you might kill performance.
the only thing you can do if he customer insists on making a search that works like this is to create a TEXT column, FULLTEXT index it, and concatenate all of the columns you want to search in to this column. if you do this, i suggest that you write your queries in this form to guarantee correct matches while maintining a sort orderthat makes sense:
select *
from sometable
where match(search_column) against('$search_terms' in boolean mode)
order
by match(search_column) against('$search_terms')

Categories