Create SQL query that orders results by which condition they meet - php

This is for MySQL and PHP
I have a table that contains the following columns:
navigation_id (unsigned int primary key)
navigation_category (unsigned int)
navigation_path (varchar (256))
navigation_is_active (bool)
navigation_store_id (unsigned int index)
Data will be filled like:
1, 32, "4/32/", 1, 32
2, 33, "4/32/33/", 1, 32
3, 34, "4/32/33/34/", 1, 32
4, 35, "4/32/33/35/", 1, 32
5, 36, "4/32/33/36/", 1, 32
6, 37, "4/37/", 1, 32
... another group that is under the "4/37" node
... and so on
So this will represent a tree like structure. My goal is to write a SQL query that, given the store ID of 32 and category ID of 33, will return
First, a group of elements that are the parents of the category 33 (in this case 4 and 32)
Then, a group of elements that are a child of category 33 (in this case 34, 35, and 36)
Then the rest of the "root" categories under category 4 (in this case 37).
So the following query will return the correct results:
SELECT * FROM navigation
WHERE navigation_store_id = 32
AND (navigation_category IN (4, 32)
OR navigation_path LIKE "4/32/33/%/"
OR (navigation_path LIKE "4/%/"
AND navigation_category <> 32))
My problem is that I want to order the "groups" of categories in the order listed above (parents of 33 first, children of 33 second, and parents of the root node last). So if they meet the first condition, order them first, if they meet the second condition order them second and if they meet the third (and fourth) condition order them last.
You can see an example of how the category structure works at this site:
www.eanacortes.net
You may notice that it's fairly slow. The current way I am doing this I am using magento's original category table and executing three particularly slow queries on it; then putting the results together in PHP. Using this new table I am solving another issue that I have with magento but would also like to improve my performance at the same time. The best way I see this being accomplished is putting all three queries together and having PHP work less by having the results sorted properly.
Thanks
EDIT
Alright, it works great now. Cut it down from 4 seconds down to 500 MS. Great speed now :)
Here is my code in the Colleciton class:
function addCategoryFilter($cat)
{
$path = $cat->getPath();
$select = $this->getSelect();
$id = $cat->getId();
$root = Mage::app()->getStore()->getRootCategoryId();
$commaPath = implode(", ", explode("/", $path));
$where = new Zend_Db_Expr(
"(navigation_category IN ({$commaPath})
OR navigation_parent = {$id}
OR (navigation_parent = {$root}
AND navigation_category <> {$cat->getId()}))");
$order = new Zend_Db_Expr("
CASE
WHEN navigation_category IN ({$commaPath}) THEN 1
WHEN navigation_parent = {$id} THEN 2
ELSE 3
END, LENGTH(navigation_path), navigation_name");
$select->where($where)->order($order);
return $this;
}
Then I consume it with the following code found in my Category block:
// get our data
$navigation = Mage::getModel("navigation/navigation")->getCollection();
$navigation->
addStoreFilter(Mage::app()->getStore()->getId())->
addCategoryFilter($currentCat);
// put it in an array
$node = &$tree;
$navArray = array();
foreach ($navigation as $cat)
{
$navArray[] = $cat;
}
$navCount = count($navArray);
$i = 0;
// skip passed the root category
for (; $i < $navCount; $i++)
{
if ($navArray[$i]->getNavigationCategory() == $root)
{
$i++;
break;
}
}
// add the parents of the current category
for (; $i < $navCount; $i++)
{
$cat = $navArray[$i];
$node[] = array("cat" => $cat, "children" => array(),
"selected" => ($cat->getNavigationCategory() == $currentCat->getId()));
$node = &$node[0]["children"];
if ($cat->getNavigationCategory() == $currentCat->getId())
{
$i++;
break;
}
}
// add the children of the current category
for (; $i < $navCount; $i++)
{
$cat = $navArray[$i];
$path = explode("/", $cat->getNavigationPath());
if ($path[count($path) - 3] != $currentCat->getId())
{
break;
}
$node[] = array("cat" => $cat, "children" => array(),
"selected" => ($cat->getNavigationCategory() == $currentCat->getId()));
}
// add the children of the root category
for (; $i < $navCount; $i++)
{
$cat = $navArray[$i];
$tree[] = array("cat" => $cat, "children" => array(),
"selected" => ($cat->getNavigationCategory() == $currentCat->getId()));
}
return $tree;
If I could accept two answers I would accept the first and last one, and if I could accept an answer as "interesting/useful" I would do that with the second.
:)

A CASE expression should do the trick.
SELECT * FROM navigation
WHERE navigation_store_id = 32
AND (navigation_category IN (4, 32)
OR navigation_path LIKE "4/32/33/%/"
OR (navigation_path LIKE "4/%/"
AND navigation_category <> 32))
ORDER BY
CASE
WHEN navigation_category IN (4, 32) THEN 1
WHEN navigation_path LIKE "4/32/33/%/" THEN 2
ELSE 3
END, navigation_path

Try an additional derived column like "weight":
(untested)
(IF(criteriaA,1,0)) + (IF(criteriaB,1,0)) ... AS weight
....
ORDER BY weight
Each criteria increases the "weight" of the sort.
You could also set the weights distinctly by nesting IFs and giving the groups a particular integer to sort by like:
IF(criteriaA,0, IF(criteriaB,1, IF ... )) AS weight

Does MySQL have the UNION SQL keyword for combining queries? Your three queries have mainly non-overlapping criteria, so I suspect it's best to leave them as essentially separate queries, but combine them using UNION or UNION ALL. This will save 2 DB round-trips, and possibly make it easier for MySQL's query planner to "see" the best way to find each set of rows is.
By the way, your strategy of representing the tree by storing paths from root to tip is easy to follow but rather inefficient whenever you need to use a WHERE clause of the form navigation_path like '%XYZ' -- on all DBs I've seen, LIKE conditions must start with a non-wildcard to enable use of an index on that column. (In your example code snippet, you would need such a clause if you didn't already know that the root category was 4 (How did you know that by the way? From a separate, earlier query?))
How often do your categories change? If they don't change often, you can represent your tree using the "nested sets" method, described here, which enables much faster queries on things like "What categories are descendants/ancestors of a given category".

Related

How to check if a value is greater than within an array

I have the following SQL statement:
$query = "SELECT item, COUNT(*) as number FROM shop GROUP BY item";
This will give me the following result:
item number
item1 23
item2 15
item3 4
I want to use this to make menu items, so normally the menu would look:
item1
item2
item3
But I want to do a check if an item has less than 10 records, that I don't want to display this item.
So in this example, the menu would be like:
item1
item2
Any idea how to achieve this?
I would like to do this in PHP because I need all the items in the query but will only want to show them which are greater then 10 and need the other items later on.
If you want to do this in PHP then you can do like this
function filterArray($value){
return ($value.number > 10);
}
$filteredArray = array_filter($yourDBArray, 'filterArray');
foreach($filteredArray as $k => $v){
//your desired array
}
In terms of speed Mysql option is good as suggested above.
Just change your query from
SELECT item, COUNT(*) as number FROM shop GROUP BY item
to
SELECT item, COUNT(*) as number FROM shop GROUP BY item HAVING number>=10
As you really need to perform this in PHP you could use array_filter() which, using a closure, will remove items which number is less than 10:
$more_than_ten = array_filter($items, function ($i) { return $i['number'] >= 10; });
Doing it with SQL would be a better solution (about performances). In case you'd need it, you could use the HAVING clause (you can't perform a WHERE number >= 10):
SELECT
item,
COUNT(*) as number
FROM shop
GROUP BY item
HAVING number >= 10
I noticed php is tagged. For the sake of options, here's how I'd go about separating the unneeded data in php if you were to get it from the database as-is:
foreach ($data as $item) {
$num = (int) $item['number']; // force of habit
if ($num >= 10) {
// display it
}
}
I'd probably separate the data at the database step, but this works if it's the route you want to take.
There is two options to filter the data so only the rows with more then 10 will appear.
At the SQL query
__
SELECT item, COUNT(*) as number FROM shop GROUP BY item HAVING number > 9
This will cause you to recieve only the requested rows from the database
Filter with PHP - every time you want to print the menu or testing it out, just can the value of 'number' in the array reutrned from the query. You can also allocate new array and insert all the values that contains 'number' that bigger then 10.

Changing the position of nodes with CakePHP 2.x Tree and jstree

I have an application using CakePHP 2.x and jstree 3.2.1. I'm trying to figure out how it's possible to specify the position of a node when writing it to the database with CakePHP. The position itself comes from jstree...
When I drag and drop items with jstree the request URL gives me:
id - The ID of the node being dragged.
parent_id - The (parent) ID which the node has been dropped under.
position - this is an integer which starts at 0 and represents the position at which id has been dropped under parent_id. For example, a position of 2 means it should appear in 3rd position (3rd, not 2nd, because they start at 0).
CakePHP has methods in it's Tree Behaviour that allow you to move nodes in its Tree up and down. These methods are called moveUp() and moveDown() respectively.
I don't understand how it's possible to use the data provided from jstree with Cake's Tree behaviour such that you could update/save positions correctly.
Passing position to moveUp() or moveDown() would produce the wrong result. Why?
If jstree provides position = 2 and you were moving the 10th item in a list calling moveUp(2) through CakePHP means it would go into the 8th position, not the 2nd as intended. Similarly, moveDown(2) would move it to the 12th, which is not the desired outcome.
The schema that Cake has for it's Tree Behavior does not have a "position" field. Rather it uses lft and rght fields. The schema has:
id - ID of each individual tree node (auto increment)
parent_id - parent ID of the node. NULL if top level (no parent).
lft, rght - for MPTT logic. Cake generates these values automatically. They can be used to determine the order using ORDER BY lft ASC. But they are not the same values as position provided by jstree, and are unique for every row
name - text name of the node.
For example, consider the following tree:
D (id = 149)
1 (id = 150, parent_id = 149)
2 (id = 153, parent_id = 149)
3 (id = 154, parent_id = 149)
4 (id = 155, parent_id = 149)
5 (id = 156, parent_id = 149)
In the database Cake stores this as follows:
If I was to use jstree to drag and drop "2" so it appears between "3" and "4" it would make a request containing the following GET variables:
id = 153. This is the ID of "2"
parent_id = 149. This is the ID of "D" which is the parent node of "2".
position = 2. This means the 3rd position (3rd because positions start from 0).
But I cannot regenerate the lft and rght values from this data? And moveUp()/moveDown() are not helpful here because position cannot be passed in a way that would make this work.
The tree should be in the following order. id and parent_id should not change, but lft and rght must, because everything under "D" has effectively been re-ordered:
D
1
3
2 (moved)
4
5
Can anyone help with this?
I never used jsTree, but if it gives you the new parent ID and the sort position, then you should be able to use a combination of saving the new parent:
$Model->save(array('parent_id' => $parentId));
afterwards obtaining its child list, which will include the new child:
$children = $Model->children($parentId, true, array('id'));
and using the sort position to determine the delta to move the modified child:
$childIds = Hash::extract($children, '{n}.Model.id');
$positionMap = array_flip($childIds);
$currentPosition = $positionMap[$nodeId];
$delta = (int)$position - $currentPosition;
if ($delta !== 0) {
if ($delta < 0) {
$Model->moveUp($nodeId, abs($delta));
} else {
$Model->moveDown($nodeId, abs($delta));
}
}
This is just some rough example that should illustrate the idea of how this could work, it assumes $parentId to be the ID of the new parent, $nodeId to be the ID of the row being moved, and $position the (zero based) position the child was moved to. You'd have to account for other situations too, like for example when the parent ID doesn't change, ie only the sort position changes, and this should all be done in a transaction!
Here's a complete snippet based on the Cookbook example, it will move the Gwendolyn node to the second position in the Work node:
$nodeId = 8;
$parentId = 9;
$position = 1;
$dataSource = $this->Category->getDataSource();
$dataSource->begin();
$this->Category->id = $nodeId;
if (!$this->Category->save(array('parent_id' => $parentId))) {
$dataSource->rollback();
} else {
$children = $this->Category->children($parentId, true, array('id'));
$childIds = Hash::extract($children, '{n}.Category.id');
$positionMap = array_flip($childIds);
$result = true;
if (isset($positionMap[$nodeId])) {
$currentPosition = $positionMap[$nodeId];
$delta = (int)$position - $currentPosition;
if ($delta !== 0) {
if ($delta < 0) {
$result = $this->Category->moveUp($nodeId, abs($delta));
} else {
$result = $this->Category->moveDown($nodeId, abs($delta));
}
}
}
if ($result) {
$dataSource->commit();
} else {
$dataSource->rollback();
}
}

Placing a user in a binary tree

How can I find where to place users in my binary tree? considering the following binary tree:
1
/ \
2 3
/ \ / \
4 5 6 7
/ \ / \
8 9 10 11
How can I build a function to return where the next user should be placed?
The rule here is that a user must be placed up as high as possible in the tree under their sponsor with each node getting them evenly. So for example, for anyone coming in with 1 as their sponsor: the next user should go to 6, the one after to 7, then to 6, then to 7, then to 8.
If someone had a sponsor of 5, they would go under 10. Sponsor of 2, they would go under 8, etc.
I've been stuck for hours trying to figure this out, and built a recursive function but it would just go down the left side and land on 8 first (as it checks 1, then 2, then 4, then 8)
My model for the tree is pretty basic (if relevant), its a table with a self-relationship like so:
id | user_id | parent_id
So each user can only have 2 children (2 entries with their id as a parent_id)
Typically when you do recursion, you end up with exactly what you got, you fly down one side (in this case the leftmost) checking everything as you go and then end up finding something way down at the bottom of the tree.
What you want to do is use a First In First Out ("FIFO") stack. You start out by adding your tree to the stack and while there is anything on the stack and you still haven't found space, you loop. Each loop you get the first item off the stack and loop again to check each of it's children for space. If you find space, you set the child aside, if you didn't find space, you add the child to the end of the stack so we check the child's children on a later iteration. You keep doing this until you find some space. After checking all of the children nodes, if any space was found you compare the nodes set aside to see which has the most free space and that is your target. This lets you search horizontally by pulling stuff off the start of the stack and adding new things to the end. You end up checking each neighbor before the children.
Here is a sample that will return a reference to the first open node found:
<?php
$tree = [
1 => [
2 => [
4 => [
8 => [],
9 => [],
],
5 => [
10 => [],
11 => [],
],
],
3 => [
6 => [],
7 => [],
],
]
];
function &findNextLocation(&$tree, $maxChildren=2){
//shortcut, check the root tree first
if(count($tree) < $maxChildren){
return $tree;
}
//fifo stack, start with the whole tree.
//we use a FIFO stack here so that we can check all nodes horizontally
//before traversing down another level. This stores all the child nodes
//we still need to search.
$stack = array(&$tree);
//potential place with space
$out = null;
//go through and check everything
//loop while there is something on the stack and we haven't
//found space yet ($out is null).
//we check $out here so that we stop as soon as somewhere with
//space is found and assigned.
while(is_null($out) && !empty($stack)){
//get the first node from the tree
$branch = &$stack[0];
array_shift($stack);
//loop over every node at this branch and look for space
foreach($branch as $id=>&$node){
//if there is space, assign it to our output variable
if(count($node) < $maxChildren){
//check the number of open spaces compared to our out spaces
if(is_null($out) || count($out) > count($node)){
//better spot, assign this node
$out = &$node;
//if anyone has zero children, we can't find less than that so break
if(count($out) == 0){
break;
}
}
} else {
//space not found here, add to our stack so we check the children
$stack[] = &$node;
}
}
}
//not found
return $out;
}
print_r($tree);
//loop a few more times starting at our next number
for($i=12; $i<=20; $i++){
//get a reference to the open node
$node = &findNextLocation($tree);
//add this node as a child
$node[$i] = [];
//remove reference to the found node to prevent errors.
unset($node);
}
print_r($tree);
You can see a demo here: https://3v4l.org/X3BX4
The way you are describing this, it sounds like it is in an SQL table. You can use the above to find a position and insert a new value to an existing tree. Or if you add another column to the table that is depth meaning the distance from the root node, you can do it all in a query. The depth can't easily be calculated because it would require recursion which isn't really available with a procedure or user function. However, it is easy to update your current records by just parent depth+1. With a depth column, you can do something like this:
SELECT *
FROM tree_table as tt
WHERE
#where there is space (less than 2 children)
(SELECT COUNT(*)
FROM tree_table
WHERE parent_id=tt.id) < 2
ORDER BY
#distance from top of tree
depth,
#by free space
(SELECT COUNT(*)
FROM tree_table
WHERE parent_id=tt.id),
#leftmost
id
Demo here: http://sqlfiddle.com/#!9/5f6bfc/5

Laravel ManyToMany to same Model

Okay guys, i am trying to rephrase my question. Lets see how try 2 goes :)
I have a artisan command to calculate the total earnings for every user.
Every day a User gets earnings assigned. But sometime, how should I describe it, he made the earnings with stuff from another user.
And thats the assert_user_id column. If a user (user 2) made earnings with stuff from another user (user 1), these earnings should be added to the earnings of user 1 in this scenario.
id, user_id, earnings, asset_user_id
1, 1, 15, null
2, 1, 43, null
3, 1, 49, null
4, 2, 32, 1
5, 2, 25, 1
6, 2, 12, null
(In this case, user 2 is making almost all of his money with content from user 1... not very nice of him, thats why the money)
The Artisan commands gets all users. and goes through them with a foreach.
inside the foreach we call a method on the user model
return $this->sum('earnings');
remember, I am inside the collection of user 1.
And now I want to sum all earnings where asset_user_id = 1. The problem, the collection persist only the rows of user 1.
Now the question:
How can i access all other rows from inside a collection of a user. Yeha i could just do User:all() within my function in my model. But i don't think this would be SOLID code. So how do i do it the right way?
What you will probably want to do is iterate over the collection with foreach and get the sums yourself.
$results = Result::all();
$sum1 = 0;
$sum2 = 0;
foreach($results as $result) {
if($result->creator_id == 1 && $result->status = 'r') {
$sum1 += (int)$result->earnings;
}
if($result->asset_creator_id == 1 && $result->status = 'r') {
$sum2 += (int)$result->earnings;
}
}
There is also a sum method in the Collection class which might be a bit easier. Just send it a callback which returns the value you are looking for. We would have to do it twice though, once for each sum you are looking for.
$sum1 = $results->sum(function($result)
{
if($result->creator_id == '1' && $result->status = 'r') {
return $result->earnings;
}
});
$sum2 = $results->sum(function($result)
{
if($result->asset_creator_id == '1' && $result->status = 'r') {
return $result->earnings;
}
});
echo "Creator Earnings: ".$sum1;
echo "Asset Creator Earnings: ".$sum2;
Sorting, counting, summing over collection would be always worse solution than doing it with db query.
Anyway your question is messy, you ask for status=r, write total for status=f, so just a guess, to show you how you can accomplish that with Eloquent features:
public function getEarningsForAssetCreator($id)
{
return $this->where('asset_creator_id', $id)
->where('status', 'f')
->sum('earnings');
}

How to interact with preview and save using flourishlib's fRecordSet's build method?

Let's suppose I have a database table called local_ads.
Now, when a local ad is created, one has to be able to view its preview and if he is satisfied, then save it. Also, if one wants to update a local ad, then he might want to see its preview before he overwrites the live version of the record.
So, I have a foreign key for the local_ads table called parent_id. If this is null, then it is a preview (at least according to my initial thoughts). Otherwise it is live. When one saves a preview, there are two cases:
Case 1: There was no live record yet linked to the preview. In this case a new record is inserted into the local_ads table with the parent_id pointing to the preview.
Case 2: There is a live record linked to the preview. In this case the live record is updated.
Everything looks and works nicely, but I have a problem showing the results in a grid. I want to show the preview if no live version of the record exists and only the live version if it exists. I would like to show something in the spirit of
select col1, col2, col3, col4
from local_ads glob
where (not (parent_id is null))
or ((select id from local_ads temp where temp.parent_id = glob.id limit 0, 1) is null)
but I have several problems. We have a logical or (I wonder how can I use logical or between logical operands using the build method of fRecordSet of flourishlib). Also, this query is of two dimensions, it is slow. Also, I wonder how can a sub-query be executed. Also, I do not know how can I use the is operator as in is null.
So, I had to re-think my idea and I came up with the following:
select col1, col2, col3, col4
from local_ads
where parent_id < id or parent_id >= id
the idea is simple: If a preview does not have a live version, then parent_id matches the id, otherwise the parent_id of a preview is null. I know this is an ugly hack, but this was the best idea I could came up with to solve the problem and decrease memory and performance complexity.
So, the only problem which remained was to check the two logical values in the where clause separated by the logical or.
From the documentation I have seen this:
* 'column<:' => 'other_column' // column < other_column
and this:
* 'column>=:' => 'other_column' // column >= other_column
so I know how can I add these to the filters, but how am I supposed to 'or' them?
So far I have tried it this way:
public static function localAd() {
$User = Globe::load('CurrentUser');
$Smarty = Globe::load('Smarty');
//handle default options
$options = array(
'recordsPerPage' => 20,
'pageLinks' => 10,
);
$page = 0;
if (isset($_GET['p'])) {
$page = $_GET['p'];
}
//get the data
$startIndex = (isset($page)) ? $page * $options['recordsPerPage'] : 0;
$filters = array();
if ($User->getType() == 'local_admin') {
$filters['domain='] = $User->getDomain();
}
$records = fRecordSet::build('LocalAd', $filters, array('created' => 'desc'), $options['recordsPerPage'], $page + 1);
//create result object for pagination
$Result = array(
"recordsReturned" => $records->count(),
"totalRecords" => $records->count(true),
"startIndex" => intval($startIndex),
"records" => $records->export(),
'recordsPerPage' => $options['recordsPerPage'],
'pageLinks' => $options['pageLinks'],
'currentPage' => $page,
//'options' => $options
);
$Result['totalPages'] = ceil($Result['totalRecords'] / $Result['recordsPerPage']);
$Smarty->assign('Result', $Result);
$Smarty->assign('ManagerURL', '?a=localAd');
AdminView::display('Admin/LocalAd/main.tpl');
}
Note, that in some cases I have to check the domain as well.
In the meantime I have managed to solve the problem. This is how we can define the filter set to solve the problem mentioned in the question:
$filters = array();
if ($User->getType() == 'local_admin') {
$filters['domain='] = $User->getDomain();
}
$filters['parent_id<:|parent_id>=:'] = array('id', 'id');

Categories