CakePHP sub query SQL in HABTM relation - mysql

I want to suggest related products by tags and sort order by the most matched.
the HABTM model association between Product and Tag
class Product extends AppModel {
//..
var $hasAndBelongsToMany = array("Tag");
//..
}
and vice versa in Tag model. also join-table name is "products_tags".
for Ex.sample..
//just sample of Product contain Tag data
$products[0]['Tag'] = array('touch', 'phone', '3G', 'apple'); //iPhone
$products[1]['Tag'] = array('phone', '3G', 'BB'); //BB
$products[2]['Tag'] = array('touch', '3G', 'apple'); //iPad
$products[3]['Tag'] = array('3G', 'air card'); //3G air card
in this sample the most related to iPhone sort by priority are..
ipad (found in 3 tags)
BB (found in 2 tags)
aircard (only 1 matched)
How Cake's way using Model find() to get sub-query like this:
SELECT DISTINCT (product_id) AS id, (
/* sub-query counting same tags*/
SELECT COUNT(*) FROM tags as Tag
LEFT JOIN products_tags AS ProductTag ON ( Tag.id = ProductTag.tag_id )
WHERE product_id = id
AND Tag.name IN ( 'touch', 'phone', '3G', 'apple' )
) AS priority /*this is weight count by how many same tags*/
FROM `tags` as Tag
LEFT JOIN products_tags AS ProductTag ON ( Tag.id = ProductTag.tag_id )
WHERE Tag.name
IN ( 'touch', 'phone', '3G', 'apple' )
ORDER BY priority DESC
SQL query above return exactly what I needed. but I can't find the way to parse params to CakePHP's AppModel->find() method.

If the result of this sql does not use pagination (assuming that related products in a normal webpage are 3-5 entries) why don't you use this complex SQL instead?
i.e. create a function relatedProducts() in your Product model
then use $this->query($sql)
sample code will be:
class Product extends AppModel {
//..
var $hasAndBelongsToMany = array("Tag");
//..
function relatedProducts($parameters){
$sql = "select..... where ....$parameters...";
return $this->query($sql);
}
}
Then you can use it in the controller by
$this->Product->relatedProducts($some_parameters);

Related

how to optimize mysql query in phalcon

i used this query:
$brands = TblBrand::find(array("id In (Select p.brand_id From EShop\\Models\\TblProduct as p Where p.id In (Select cp.product_id From EShop\\Models\\TblProductCategory as cp Where cp.group_id_1='$id'))", "order" => "title_fa asc"));
if($brands != null and count($brands) > 0)
{
foreach($brands as $brand)
{
$brandInProductCategory[$id][] = array
(
"id" => $brand->getId(),
"title_fa" => $brand->getTitleFa(),
"title_en" => $brand->getTitleEn()
);
}
}
TblBrand => 110 records
TblProduct => 2000 records
TblProductCategory => 2500 records
when i used this code, my site donot show and loading page very long time ...
but when i remove this code, my site show.
how to solve this problem?
The issue is your query. You are using the IN statement in a nested format, and that is always going to be slower than anything else. MySQL will need to first evaluate what is in the IN statement, return that and then do it all over again for the next level of records.
Try simplifying your query. Something like this:
SELECT *
FROM Brands
INNER JOIN Products ON Brand.id = Products.brand_id
INNER JOIN ProductCategory ON ProductCategory.product_id = Products.id
WHERE ProductCategory.group_id_1 = $id
To achieve the above, you can either use the Query Builder and get the results that way
https://docs.phalconphp.com/en/latest/api/Phalcon_Mvc_Model_Query_Builder.html
or if you have set up relationships in your models between brands, products and product categories, you can use that.
https://docs.phalconphp.com/en/latest/reference/model-relationships.html
example:
$Brands = Brands::query()
->innerJoin("Products", "Products.brand_id = Brand.id")
->innerJoin("ProductCategory", "ProductCategory.product_id = Products.id")
->where("ProductCategory.group_id_1 = :group_id:")
->bind(["group_id" => $id])
->cache(["key" => __METHOD__.$id] // if defined modelCache as DI service
->execute();
$brandInProductCategory[$id] = [];
foreach($Brands AS $Brand) {
array_push($brandInProductCategory[$id], [
"id" => $Brand->getId(),
"title_fa" => $Brand->getTitleFa(),
"title_en" => $Brand->getTitleEn()
]);
}

How to simplify this 2 MySQL Queries

This must be an easy one, but I can't remember how I did it before.
I'm trying to make another query within the query where record IDs are match:
/*
tables:
models:
model_id
model_name
models_years:
model_year_id
model_id
year
*/
$models = DBW::run('SELECT * FROM models', [], true);
$models_years = DBW::run('SELECT * FROM models_years', [], true);
$output = [];
foreach ($models as $model)
{
$years = [];
foreach ($models_years as $model_year)
{
if ($model_year['model_id'] == $model['model_id'])
{
$years[] = $model_year['year'];
}
}
$output[] = [
'model_name' => $model['model_name'],
'years' => $years
];
}
var_dump( $output );
I use PDO (settings: ATTR_DEFAULT_FETCH_MODE = FETCH_ASSOC), and "DBW::run" function returns $stmt->fetchAll().
this is just an example of what I'm trying to do or improve, I know it's possible to do all of that in a single SQL, I've done it before and can't remember! :(
SELECT m.model_name, y.`year`
FROM models AS m
LEFT JOIN models_years As y ON m.model_id = y.model_id
OR from your original question, the following might be what you need:
SELECT m.model_name, group_concat(y.`year`) AS `year`
FROM models AS m
LEFT JOIN models_years As y ON m.model_id = y.model_id
GROUP BY 1
Why?
1 - Join table to reduce SQL calls
2 - GROUP the result (BY 1 == BY m.model_name, just a lazy shorthand here)
3 - group_concat(...) will by default produce: year1,year2,year3,... and then you can use PHP explode(',', ...) to change to array if you need
Join the tables, and use GROUP_CONCAT to combine all the years for each model. Then use explode() to split that into an array.
$output = DBW::run('select m.model_name, GROUP_CONCAT(y.year) AS years
FROM models AS m
LEFT JOIN models_years AS y ON m.model_id = y.model_id
GROUP BY m.model_id', [], true);
foreach ($output as &$row) {
$row['years'] = explode(',', $row['years']);
}
SELECT model_name, year FROM models, models_years WHERE
models.model_id=models_years.model_id
Or, if you prefer the JOIN syntax,
SELECT model_name, year FROM models JOIN models_years ON
models.model_id=models_years.model_id
another way to do this is with the sub select.
select * from models where model_id in (select model_id from models_years)

Yii2: How to select multiple fields from different tables

My tables
Category
id_category
name
Post
id_post
category_id
title
My query:
Post::find()
->select('post.*, c.name AS catname')
->leftJoin('category c', 'c.id_category = category_id')
->all();
The output just shown the table fields Post, is not the field catname.
1) Define a relation in Post model named 'category', so:
public function getCategory() {
return $this->hasOne(Category::className(), ['id_category' => 'category_id']);
}
2) Then when you query the posts, use 'with' if you need to get category name for each post:
$posts = Post::find()
->with('category')
->all();
3) You can access to category name with:
$post->category->name

Query 2 tables with one field linked to 2 different values

I'm trying to make a SQL query and I have some problems with it.
CREATE table entries (
id_entry INT PRIMARY KEY,
);
CREATE table entry_date (
entry_date_id INT PRIMARY KEY,
entry_id INT,
entry_price INT,
entry_date TEXT,
);
for each entry, there is several dates.
I'd like to select the entries.entry_id where that entry have, for example, the dates '23/03/2013' and '24/03/2013' linked to it.
The two dates are stored into an array:
$data = array('ci' => '23/03/2013', 'co' => '24/03/2013');
I store the dates in text for practical purpose in my treatment.
I use Zend_Db so my query is constructed like that:
$select = $table->select ()->from ( 'entries' )->setIntegrityCheck ( false );
if ($data ['ci'] != NULL) {
$select->join ( array (
'entry_dates' => 'entry_dates'
), 'entries.id_entry = entry_dates.entry_id' );
$select->where ( 'entry_dates.entry_date = ?', $data ['ci'] );
}
if ($data ['co']) {
if ($data['ci'] == NULL) {
$select->join ( array (
'entry_dates' => 'entry_dates'
), 'entries.id_entry = entry_dates.entry_id' );}
$select->where ( 'entry_dates.entry_date = ?', $data ['co'] );
}
which gives :
SELECT `entries`.*, `entry_date`.*
FROM `entries`
INNER JOIN `entry_dates`
ON entries.id_entry = entry_dates.entry_id
WHERE (entry_dates.entry_date = '23/03/2013')
AND (entry_dates.entry_date = '24/03/2013')
And, well ... It doesn't work.
When I fetch my $select, I get nothing.
I guess I miss something in my request when I do the WHERE ... AND , what should I do to get the correct output ? The real request being really long, I'd like to avoid another long subselect if possible.
It can be done in two way, either with a self-join on the entry_date table:
SELECT `entries`.entry_id
FROM `entries`
INNER JOIN `entry_dates` AS ed1
ON entries.id_entry = ed1.entry_id
INNER JOIN `entry_dates` AS ed2
ON entries.id_entry = ed2.entry_id
WHERE ed1.entry_date = '23/03/2013'
AND ed2.entry_date = '24/03/2013'
Or with an aggregate
SELECT `entries`.entry_id
FROM `entries`
INNER JOIN `entry_dates` AS ed
WHERE ed.entry_date = '23/03/2013'
OR ed2.entry_date = '24/03/2013'
GROUP BY `entries`.entry_id
HAVING COUNT(DISTINCT ed.entry_date)=2

How to combine sql select queries for this situation?

i'm writing a chat app with php/mysql
i have 3 tables: user, room and room_participant with these structures:
user: id, username
room: id, title
room_participant: room_id, user_id
Now i want to get list of all rooms along with list of all participants in each room.
Until now i just select all rooms from room table and iterate through all rooms and select users information out of each entry, which is very inefficient.
Is there any way to combine all these select into only 1 select query?
Not certain about this without testing, but give it a try:
SELECT
room.*,
user.*
FROM room
JOIN room_participant ON room_id = room_participant.id
JOIN user ON room_participant.user_id = user.id
ORDER BY room.id
To deduplicate rooms, use GROUP_CONCAT()
UPDATE GROUP_CONCAT() modified to return id|username
SELECT
room.id, room.name
GROUP_CONCAT(CONCAT(user.id,'|',user.username)) AS userlist
FROM room
JOIN room_participant ON room_id = room_participant.id
JOIN user ON room_participant.user_id = user.id
GROUP BY room.id, room.name
ORDER BY room.id
With the userlist generated by GROUP_CONCAT as id|name,id|name,id|name you can use PHP explode() to separate them.
// Split the list on the commas
$users = explode(",", $userlist);
$final_users = array();
// Then split each on the `|`
foreach ($users as $user) {
$split_user = explode("|", $user);
// Append each as a new associative array to $final_users
$final_users[] = array('id' => $split_user[0], 'username' => $split_user[1]);
}
// Now you have an array of users as id, username
var_dump($final_users);
You may wish to do two queries, and then match things up in whatever is using MySQL, e.g. PHP.
These two:
SELECT id, title
FROM room;
SELECT rp.room_id, rp.user_id, u.username
FROM room_participant AS rp
INNER JOIN user as u ON rp.user_id = u.id;
Or these two:
SELECT id, username
FROM user;
SELECT rp.room_id, rp.user_id, r.title
FROM room_participant AS rp
INNER JOIN room as r ON rp.user_id = r.id;
Which two queries make sense depends on what you're doing with the info really.
You could go a step further and select all three separately:
SELECT *
FROM room;
SELECT *
FROM user;
SELECT *
FROM room_participant;
Note: It's probably better to state the columns, rather than using '*', just in case in the future new columns are added to the table that you're not really interested in for these queries.
Obviously, you'd then have to match everything up in whatever is using MySQL, e.g. PHP. You could create a list of rooms and users from the selected info, and then match them up with something like:
// Use MySQL to populate $roomList from database, then do...
foreach ($roomList as $room)
{
$id = $room['id'];
$title = $room['title'];
$this->roomList[$id] = new Room($id, $title);
}
// Use MySQL to populate $userList from database, then do...
foreach ($userList as $user)
{
$id = $user['id'];
$username = $user['username'];
$this->userList[$id] = new User($id, $username);
}
// Use MySQL to populate $roomParticipantList from database, then do...
foreach ($roomParticipantList as $roomParticipant)
{
$room = $this->roomList[$roomParticipant['room_id']];
$user = $this->userList[$roomParticipant['user_id']];
// You could do one/both of these, depending on requirements.
$room->enterUser($user);
$user->joinRoom($room);
}