Pivoting mysql result with dates and sums - php

I have some data in mysql that I need to create some reports from.
My data are coming from the following query :
SELECT StoreNbr,StoreName,Date, SUM(`Sales`) FROM sales_tbl GROUP BY StoreNbr,StoreName,Date;
This results in the following data (just a small subset for my example):
+-----------+---------------------------+----------------------------+
| StoreNbr | StoreName | Date | SUM(Sales) |
+-----------+---------------------------+----------------------------+
| 1112 | Store1 | 2016-01-16 | 115.09 |
| 1112 | Store1 | 2016-01-17 | 81.00 |
| 1113 | Store2 | 2016-01-16 | 112.44 |
| 1113 | Store2 | 2016-01-17 | 56.61 |
I would like to transform my data to be this way :
+-----------+---------------------------+----------------------------+
| StoreNbr | StoreName | 2016-01-16 | 2016-01-17 |
+-----------+---------------------------+----------------------------+
| 1112 | Store1 | 115.09 | 81.00 |
| 1113 | Store2 | 112.44 | 56.61 |
Obviously there might be thousands of rows (stores) and unknown number of dates to be returned in the query as my query might be run like this (this will need to return 120+ number of columns for dates):
SELECT StoreNbr,StoreName,Date, SUM(`Sales`) FROM sales_tbl WHERE (Date BETWEEN '2016-01-10' AND '2016-05-10') GROUP BY StoreNbr,StoreName,Date;
There are a few ways to do this, none very simple. I did some research and there are some that mention that mysql does not support pivoting. I am running mariadb though, and saw that mariadb supports pivoting through the connect engine. I was unable to make it work though (adjust their official examples on my data).
Another way is lots of IFs and Cases, but most of the answers I am finding are very difficult to adapt or are tailored only to the data the guy that asks provides.
Another approach would be to process the data as they come out on my array as I have a json response in the end that feeds a datatable. - This is another think I have not managed to figure out yet.
I am looking for a way to get the desired output independent on the amount of dates (and I guess dates could be replaced by weeks or whatever else). Can anyone help?

Select all distinct dates
SELECT DISTINCT Date
FROM sales_tbl
WHERE (Date BETWEEN '2016-01-10' AND '2016-05-10')
ORDER BY Date;
and initialize an array which is indexed by that dates storing zeros:
$dateIndexedArray = array();
while($row = $stmt1->fetch(PDO::FETCH_ASSOC) {
$dateIndexedArray[$row['Date']] = 0;
}
The arry will look like
[
'2016-01-16' => 0,
'2016-01-17' => 0
]
Then execute your query
SELECT StoreNbr, StoreName,Date, SUM(`Sales`) AS Sales
FROM sales_tbl
WHERE (Date BETWEEN '2016-01-10' AND '2016-05-10')
GROUP BY StoreNbr,StoreName,Date;
And store the "Sales" in a date indexed array per store
$report = array();
while($row = $stmt2->fetch(PDO::FETCH_ASSOC)){
$storeIndex = $row['StoreNbr'] . ':' . $row['StoreName'];
if (!isset($report[$storeIndex])) {
$report[$storeIndex] = array(
'StoreNbr' => $row['StoreNbr'],
'StoreName' => $row['StoreName'],
'Sales' => $dateIndexedArray
);
}
$report[$storeIndex]['Sales'][$row['Date']] = $row['Sales'];
}
The $report array will look like:
[
'1112:Store1' => [
'StoreNbr' => 1112,
'StoreName' => 'Store1',
'Sales' => [
'2016-01-16' => 115.09,
'2016-01-17' => 81.00
]
],
'1113:Store2' => [
'StoreNbr' => 1113,
'StoreName' => 'Store2',
'Sales' => [
'2016-01-16' => 112.44,
'2016-01-17' => 56.61
]
]
]
Update:
If you need all data to be in one row for each store you can change the code to:
$report = array();
while($row = $stmt2->fetch(PDO::FETCH_ASSOC)){
$storeIndex = $row['StoreNbr'] . ':' . $row['StoreName'];
if (!isset($report[$storeIndex])) {
$report[$storeIndex] = $dateIndexedArray;
$report[$storeIndex]['StoreNbr'] = $row['StoreNbr'];
$report[$storeIndex]['StoreName'] = $row['StoreName'];
}
$report[$storeIndex][$row['Date']] = $row['Sales'];
}
The resulting array will look like:
[
'1112:Store1' => [
'StoreNbr' => 1112,
'StoreName' => 'Store1'
'2016-01-16' => 115.09,
'2016-01-17' => 81.
],
'1113:Store2' => [
'StoreNbr' => 1113,
'StoreName' => 'Store2',
'2016-01-16' => 112.44,
'2016-01-17' => 56.61
]
]
Update 2: To get the total sales per store you can use WITH ROLLUP
SELECT StoreNbr, StoreName,Date, SUM(`Sales`) AS Sales
FROM sales_tbl
WHERE (Date BETWEEN '2016-01-10' AND '2016-05-10')
GROUP BY StoreNbr,StoreName,Date WITH ROLLUP;
$report = array();
while($row = $stmt2->fetch(PDO::FETCH_ASSOC)){
if ($row['StoreName'] === null) {
// Skip this row.
// It contains total sales grouped by StoreNbr
// (or not grouped if StoreNbr === null).
continue;
}
$storeIndex = $row['StoreNbr'] . ':' . $row['StoreName'];
if (!isset($report[$storeIndex])) {
$report[$storeIndex] = $dateIndexedArray;
$report[$storeIndex]['StoreNbr'] = $row['StoreNbr'];
$report[$storeIndex]['StoreName'] = $row['StoreName'];
}
if ($row['Date'] === null) {
// This row contains total sales grouped by StoreNbr & StoreName
$report[$storeIndex]['TotalSales'] = $row['Sales']
} else {
$report[$storeIndex][$row['Date']] = $row['Sales'];
}
}
Please note that i've never used WITH ROLLUP. So you might need to adjust the code.

Related

Cakephp 4: FIND_IN_SET not returning result

I have an array, $submenus, in my app that I implode to a delimited string:
$subs = implode(',', $submenus);
The string will look something like this: 'ml_,nc_,msr_'. These values are stored in a field called group_prefix in my submenus table. Each submenu row has a unique group_prefix.
The following code builds menus and submenus from a database:
$menus = $this->Menus->find('all', [
'order' => ['Menus.display_order ASC'],
'conditions' => $conditions,
'contain' => [
'Submenus' => [
'conditions' => [
'Submenus.status' => 1,
'FIND_IN_SET("' . $subs . '", Submenus.group_prefix)'
],
]
]
]);
$this->set('menus', $menus);
It works fine until I add the FIND_IN_SET condition on Submenus. When I do, I get no submenus returned, just the main menus. Debug confirms that the string is formatted propery. Doesn't error out, I just get no resultset.
When I run the submenus query in MySQL, it works.
set #prefixes = 'ml_,nc_,msr_';
SELECT `id`, `name` FROM `submenus` WHERE `status` = 1 AND FIND_IN_SET(`submenus`.`group_prefix`, #prefixes);
+----+---------------------------+
| id | name |
+----+---------------------------+
| 4 | Mission Lessons Module |
| 5 | MSR Module |
| 8 | Work Authorization Module |
+----+---------------------------+
What am I missing?
Answer was to reverse the order of arguments in FIND_IN_SET.

(Laravel/PHP) SQL Query that selects all orders from table that matches product_id and variation_id in array

Im trying to build a SQL Query that will select all orders from a table that matches options that i defined.
Databse i use: Mysql
Language: PHP
Basicly i have a array that looks like this.
[
[
"user_id" => 1,
"product_id" => 5548,
"variation_id" => 14
],
[
"user_id" => 1,
"product_id" => 5548,
"variation_id" => 15
],
[
"user_id" => 1,
"product_id" => 4422,
"variation_id" => 4
]
]
This means that the user(id: 1) has one product with the "id" of 5548, and then he also has 2 variations of that product that are "id" 14 and 15. You can also see that the same user owns the product(id:4422) that has variation(id:4).
I then have a "order_lines" table that looks like this
order_lines
+----+-----+---------+-----------------------------+
| id | uid | user_id | product_id | variation_id |
+----+-----+---------+-----------------------------+
| 1 | 1 | 1 | 5548 | 14 |
+----+-----+---------+-----------------------------+
| 2 | 2 | 1 | 5548 | 15 |
+----+-----+---------+-----------------------------+
| 3 | 3 | 1 | 4422 | 4 |
+----+-----+---------+-----------------------------+
| . | . | . | .... | .. |
+----+-----+---------+-----------------------------+
I now need a SQL Query that selects all the rows where there is a match between the user_id, product_id and variation_id that are defined in the array.
The output should contain all rows that meet these conditions.
I hope someone can pin me in the right direction.
I'm building in Laravel if you got the query builder just at your hand. Else i very much appreciate an SQL Query.
if I am getting you right, below code will help you, using just Core PHP
foreach($array as $arr){
$user_id = $arr['user_id'];
$prodct_id = $arr['prodct_id'];
$variation_id = $arr['variation_id'];
$query = "SELECT * FROM order_lines WHERE user_id = $userId AND product_id = $productId AND variation_id = $variationId";
$queryResult = mysql_fetch_assoc($query);
$yourCollection[] = $queryResult;
}
print_r($yourCollection);
Try below code to use Laravel Query Builder, below code will help you to get results for multiple users based on product and variation.
$qb_order_lines = DB::table('order_lines');
$where_condition = [
['user_id' => '', 'product_id' => '', 'variation_id' => ''],
];
foreach ($where_condition as $condition) {
$qb_order_lines->orWhere(function($query) use ($condition) {
$query->where('user_id', $condition['user_id'])
->where('product_id', $condition['product_id'])
->where('variation_id', $condition['variation_id']);
});
}
$obj_result = $qb_order_lines->get();
If you want to get it for only one user, use below code
$obj_result = DB::table('order_lines')
->where('user_id', $condition['user_id'])
->where('product_id', $condition['product_id'])
->where('variation_id', $condition['variation_id'])
->get();
You can modify the above query builders based on your requirements like select fields or group by.
Let me know if you need any help.
For anyone interesting.
My problem was that i needed to count of many matches that were between my array and my database.
Instead of selecting and outputting. I eneded up using sql count() function in a query, that did the job.

Split date range into weeks and sum columns based on the week

So I'm calling in data from a DB where each record is associated with a (column) 'start_time', 'no_shows and 'cancelled'. Successful attendance is based on whether 'no_shows' && 'cancelled' == 0.
I want to go through the results of this SELECT and count the number of no shows, cancellations and attended on a WEEKLY basis (based on the start_time). How can I achieve this?
The user will provide a date range and the DB will select the records based on that date range. Now this date range will then have to be split by weeks and then get the count. I'm totally stuck on the counting by weeks part. This is what I have so far:
// Multidimensional Array with [boolean][week #]
$no_shows_weekly = [
'no_show' => [],
'week' => []
];
$cancelled_weekly = [
'cancelled' => [],
'week' => []
];
$attended_weekly = [
'attended' => [],
'week' => []
];
foreach($result as $r) {
$start_time = new DateTime($r['start_time']);
if($r['is_no_show'] == 0 && $r['is_cancelled'] == 0) {
array_push($attended_weekly['attended'], 1);
array_push($attended_weekly['week'], date_format($start_time, "W"));
}
else {
array_push($attended_weekly['attended'], 0);
array_push($attended_weekly['week'], date_format($start_time, "W"));
}
array_push($no_shows_weekly['no_show'], $r['is_no_show']);
array_push($no_shows_weekly['week'], date_format($start_time, "W"));
array_push($cancelled_weekly['cancelled'], $r['is_cancelled']);
array_push($cancelled_weekly['week'], date_format($start_time, "W"));
}
echo json_encode(array(
'success'=> 1,
'msg'=>
array(
'No Shows' => $no_shows_weekly,
'Cancellations' => $cancelled_weekly,
'Attendend' => $attended_weekly
)
));
I wasn't able to do any counting in this, rather I just pulled the data and separated into arrays with the corresponding week.
I want to pull data into something like this:
Week 50: {No_Shows: 10, Cancelled: 5, Attended: 25}
Week 52: {No_Shows: 10, Cancelled: 5, Attended: 25}
General DB Structure:
+------------+-----------+-----------+
| start_time | no_shows | cancelled |
+------------+-----------+-----------+
| 2019-12-20 | 1 | 0 |
| 2019-12-21 | 0 | 0 |
| 2019-12-22 | 0 | 1 |
I tried to do this in MySQL as well:
SELECT
WEEKOFYEAR('start_time') AS weekno,
SUM('is_no_show' = 1) AS no_shows,
SUM('is_cancelled' = 1) AS cancelled
FROM
myTable
WHERE
(
`start_time` > '2019-12-01' AND `start_time` < '2019-12-07'
) AND category LIKE 'Continued Probation'
GROUP BY
weekno
However, this statement is returning Null for me. Any suggestions is appreciated! Thank you

PHP processing data and create array

I have a mysql query with result like this:
ID | index | Mapping index | Date
1 | 27 | value27 | 2019-04
2 | 28 | value28 | 2019-05
3 | 28 | value28 | 2019-05
4 | 32 | value32 | 2019-07
5 | 32 | value32 | 2019-05
The results should be prepared to display stacked charts. As result i need in php:
// array to display google chart
['2019-04', 1, 0, 0,],
['2019-05', 0, 2, 1,],
['2019-07', 0, 0, 1,],
// explanation
ID | value27 | value28 | value 32 | Date
1 | 1 | 0 | 0 | 2019-04
2 | 0 | 2 | 1 | 2019-05
2 | 0 | 0 | 1 | 2019-07
This is my php script:
$preparevar = array();
foreach($data["timechart"] as $date){
array_push($preparevar,[$date->date, $date->count , '\''.$date->repcontent.'\'' ]);
}
$googleChartArray = array(); //Use this array to group the results using date.
foreach( $preparevar as $d ) {
$date = $d[0];
$value = $d[1];
if( !isset( $googleChartArray[$date] ) ) {
$googleChartArray[$date] = array( "'". $date. "'" ); //Date needs to be enclosed in quote.
}
$googleChartArray[$date][] = $value;
}
$f = array(); //Format the above array to split value in a comma separated format.
foreach( $googleChartArray as $g ) {
$f[] = implode( ',' , $g );
}
$json_out = json_encode(array_values($googleChartArray));
The problem with this format is, that the zero values will be ignored:
[
['2019-04',1],
['2019-05',2,1],
['2019-07',1]
]
should be:
[
['2019-04',1,0,0],
['2019-05',0,2,1],
['2019-07',0,0,1]
]
Here an example of $data["timechart"]:
array(11) {
[0]=>
object(stdClass)#43 (14) {
["id"]=>
string(2) "46"
["index"]=>
string(2) "31"
["index2"]=>
string(1) "0"
["keynr"]=>
string(2) "31"
["repcontent"]=>
string(41) "Value31"
["count"]=>
string(1) "1"
["date"]=>
string(7) "2007-06"
}
And here an example of my query. I canĀ“t use SUM(CASE) for example beacause index are variable.
SELECT
orders.id,
positions_list.index,
RepK.keynr,
RepK.content AS repcontent,
RepK.p_company,
COUNT(positions_list.index) AS count,
DATE_FORMAT(orders.date_placement, '%Y-%m') AS date
from orders
JOIN tools
ON tools.id=orders.tool_id
JOIN positions_list ON positions_list.order_id = orders.id
LEFT JOIN repkey as RepK
ON RepK.keynr=positions_list.index
AND RepK.p_company=orders.comp_id
WHERE
tools.id =:id
AND RepK.keynr IS NOT NULL
group by DATE_FORMAT(orders.date_placement, '%Y-%m'),positions_list.index
MySQL doesn't currently offer variable width pivots, so you can either:
make two queries, the first to collect the unique repcontent columns, then build a second query to implement a pivot technique by writing a SELECT clause with dynamic CASE WHEN statements for each column or
make one query, and let php prepare the results (this can be scripted up in a few different ways, but I'll recommend this one)
Code: (Demo)
$resultSet = [
['repcontent' => 'Value 27', 'date' => '2019-04'],
['repcontent' => 'Value 28', 'date' => '2019-05'],
['repcontent' => 'Value 28', 'date' => '2019-05'],
['repcontent' => 'Value 32', 'date' => '2019-07'],
['repcontent' => 'Value 32', 'date' => '2019-05'],
];
$columns = array_unique(array_column($resultSet, 'repcontent'));
$lookupKeys = range(1, count($columns));
$lookup = array_combine($columns, $lookupKeys);
$defaults = array_fill_keys($lookupKeys, 0);
foreach ($resultSet as $row) {
if (!isset($result[$row['date']])) {
$result[$row['date']] = array_merge([$row['date']], $defaults);
}
++$result[$row['date']][$lookup[$row['repcontent']]];
}
echo json_encode(array_values($result));
Output:
[["2019-04",1,0,0],["2019-05",0,2,1],["2019-07",0,0,1]]
For simplicity, generate a result set as an array of arrays.
Extract the unique repcontent values
Generate an array with values ranging from 1 to the unique repcontent count
Forge a lookup array consisting of #1 as keys and #2 as values -- this will determine where each "count" will stored when looping later
Create a default array consisting of #2 as keys and zeros as values
Now, loop through the result set and if a given row has a repcontent value which is encountered for the first time, create a new row in the output array using the date as the first element and the elements from #4 to follow.
Unconditionally, add 1 to the row's column that corresponds with with the repcontent value
If you don't quite understand why any of the variables ($columns, $lookupKeys, $lookup, $defaults) are generated or what they contain, call var_export() on my variables before entering the loop -- that should clear up any confusion.
I have a feeling that I could refine your query, but I won't venture a guess without having some realistic sample data to play with.
I don't see why you would need to add additional quotes to your json for the Google chart to work. If the chart doesn't render without the additional quotes, this is probably a symptom that you are passing the php variable to javascript in an improper fashion.
p.s. I see that you some development with Joomla, if this is a Joomla script and you are not able to craft your query with Joomla's query building methods, please post your best effort on Joomla Stack Exchange and I'll see if I can help.

Complicated nested array issue

I want to query data from a database into a json array that ultimalty produces a force directed graph in javascript. Here is what the Json array should be like. However nodes can have mulitiple adjacencies or none, how can I query a json array where the adjacencies section varies from node to node and is able to adjust according to the number of adjacencies a node has?
Var JSON =[
{
"adjacencies": [
{
"nodeTo": "graphnode9",
"nodeFrom": "graphnode5",
"data": {}
}
],
"data": {
"$color": "#416D9C",
"$type": "star"
},
"id": "graphnode5",
"name": "graphnode5"
},
];
or they can have
Var JSON =[
{
"adjacencies": [
{
"nodeTo": "graphnode9",
"nodeFrom": "graphnode5",
"data": {}
},
{
"nodeTo": "graphnode9",
"nodeFrom": "graphnode5",
"data": {}
},
{
"nodeTo": "graphnode9",
"nodeFrom": "graphnode5",
"data": {}
}
],
"data": {
"$color": "#416D9C",
"$type": "star"
},
"id": "graphnode5",
"name": "graphnode5"
},
];
or they can not have any
Var JSON =[
{
"adjacencies": [],
"data": {
"$color": "#416D9C",
"$type": "star"
},
"id": "graphnode5",
"name": "graphnode5"
},
];
Here is my attempt so far, however this only produces a json that only allows one adjacencies, How can I setup a Json query that will adjust the the number of adjacencies a node have? while just loading the data and id section once but allowing the adjacenies to be varied?
Here is my Database structure
nodes Relationships
----- -------------
id int(11), id int(11),
name varchar(35), goingto int(11), //this is the destination node from the id relation
color varchar(7), data varchar(0) null
type varchar (12), Foreign key (id) references nodes(id)
Primary key (id)
engine = innodb
And here is my attempt that
function getjson(){
$db = adodbConnect();
$query = "SELECT nodes.*, relationships.* FROM nodes inner JOIN relationships ON nodes.id = relationships.id";
$result = $db -> Execute($query);
while($row=$result->FetchRow())
{
$id= (float)$row['id'];
$name = $row['name'];
$color1 = $row['color'];
$type1 = $row['type'];
$to= (float)$row['goingto'];
$thumb =$row['thumb']; //image path
$array[] = array(
"adjacencies" => array( array(
"nodeTo" => "$to",
"nodeFrom" => "$id",
"data" => array() )),
"data" => array(
"$"."color" => $color1,
"$"."type" => $type1 ),
"id" => $id,
"name" => "<img src='".$thumb."' height='25' width='25' alt='root'/><label>".$name."</label>");
}
$json = json_encode($array);
print "$json";
//return $json;
}
If you want to return the result in a single query, then you will end up with duplicated data for the node, in each separate row where there's a distinct adjacency from that node... Which is fine, that's how it works.
But as it sits, you won't get nodes returned if there's no adjacency on that node (because you're using an INNER join. You should use a LEFT join to include nodes that have no results from the related adjacency table).
By sorting by node id, we explicitly ensure that all nodes and their adjacencies appear grouped together. This is probably happening already because id is your pk and hence the sort is happening this way "automatically". But an ORDER BY nodes.id ensures this happens, and makes your intention clear to anyone looking at the code.
Also, because you're returning everything * from both tables, you're going to have column name conflicts, on node.id and relationship.id. Ideally you'd explicitly name your columns to avoid this so that you have predictable results back in PHP.
So your SQL could look more like:
SELECT
n.id as n_id,
n.name,
n.color,
n.type,
r.id as r_id,
r.goingto,
r.data
FROM
nodes n
LEFT JOIN relationships r
ON n.id = r.id
ORDER BY
n.id
This returns a result set that looks something like:
n_id | name | color | type | r_id | goingto | data
------+-------+--------+-------+------+---------+-----------
1 | node1 | red | type1 | 1 | 5 | stuff
1 | node1 | red | type1 | 2 | 6 | morestuff
2 | node2 | blue | type2 | 3 | 10 | whatever
3 | node3 | green | type3 | null | null | null
4 | node4 | orange | type4 | 4 | 20 | xxx1
4 | node4 | orange | type4 | 5 | 21 | xxx2
4 | node4 | orange | type4 | 6 | 22 | xxx3
etc...
(ie this assumes node 1 has two relationships, node 2 has 1 relationship, node 3 has no relationships, and node 4 has 3).
And then, your code that builds the array just needs to iterate the results, building a new node only when the current record's node is not the same as the previous one (ie we're relying on the ORDER BY node.id to "gather" all the info for a particular node, sequentially).
This code hasn't been tested, but I think the intent is clear, you should be able to bend this as required - but it basically just implements the above.
Replace your while loop with all of this.
$previd = -1;
while($row=$result->FetchRow())
{
$id= (float)$row['n_id']; // <--- note change from 'id' to 'n_id'
$name = $row['name'];
$color1 = $row['color'];
$type1 = $row['type'];
$to= (float)$row['goingto'];
$thumb =$row['thumb']; //image path
// Is this row the start of a new node?
if ($previd != $id) {
// Yes, new node. Record our new node id, for future new node checks.
$previd = $id;
// Store the previous node we've already built, now that it's complete (but only if there was a previous node!)
if ($previd != -1) {
$array.push($node);
}
// Start our new node off, ready to accept adjacencies
$node = array(
"adjacencies" => array(),
"data" => array(
"$"."color" => $color1,
"$"."type" => $type1
),
"id" => $id,
"name" => "<img src='".$thumb."' height='25' width='25' alt='root'/><label>".$name."</label>");
}
// Any adjacency for this node, on this row?
if ($to != null) { // <-- Not sure about this line!
// Yes there is, so create the new adjacency record and add it to the current node's adjacency array.
$node["adjacencies"].push(
array(
"nodeTo" => "$to",
"nodeFrom" => "$id",
"data" => array()
)
);
}
}
I'm not sure how "no adjacency" will be represented in $to - ie if this will be "null" or what. I'll leave that to you to test, but suffice to say you'll need to reflect this in the line if ($to != null) { // <-- Not sure about this line!

Categories