Use array as table in MySql JOIN - php

I would like to use data from an array to add a column and make a join on a MySql table.
Let's say, on one hand, we have an array ($relevance):
$relevance = array(
array('product_id' => 1, 'relevance' => 2),
array('product_id' => 2, 'relevance' => 5),
array('product_id' => 3, 'relevance' => 1),
);
And on the other hand, we have this table (products):
product_id | product_name
--------------------------
1 | Product 1
2 | Product 2
3 | Product 3
Now, I want to select data from the products table and joining them with $relevance based on their product_id in order to get something like this:
product_id | product_name | relevance
---------------------------------------
1 | Product 1 | 2
2 | Product 2 | 5
3 | Product 3 | 1
In other words, how can I make a SELECT with LEFT JOIN using data from both the MySql database and an array which would "mean" something like this:
SELECT `p`.*, `{{$relevance}}`.* FROM `products` AS `p`
LEFT JOIN `{{$relevance}}`
ON p.product_id = {{$relevance}}.product_id

pure sql solution, not efficient though for big recordsets:
$relevances = array()
foreach ($relevance as $v){
$relevances[] = "SELECT {$v['product_id']} as product_id, {$v['relevance']} as relevance"
}
$sql = implode(' UNION ', $relevances);
$sql = "SELECT p.product_id, p.product_name, r.relevance
FROM products p
JOIN ($sql) r ON p.product_id=r.product_id";

Well, you can make another table relevance and then you could just use JOIN. Or you can use loop to get those data. Something like
$relevance = array(
array(1, 'relevance' => 2),
array(2, 'relevance' => 5),
array(3, 'relevance' => 1),
);
$q = mysql_query("SELECT * FROM products")
while($r = mysql_fetch_assoc($q))
{
$productRelevance = $relevance[$r['prod_id']-1];
}
Hoewever this code may fail if you delete some product and those ids wouldn' be in order, e.g.: 1,2,5,6,7,10. I recommend you to use another table.

Related

PHP/SQL Recursive Function - how to handle limited selects on mysqli while loop with sub selects

I have a table with products.
Sub-products can be assigned to a main product.
products
________________________________________________________________________
id | product_title | main_id | owner_id
1 | Volvo | 0 | 1
2 | Mercedes | 0 | 2
3 | Chevrolet | 0 | 1
4 | Rear lights | 1 | 1
5 | Glasses | 1 | 1
6 | Seats | 1 | 1
7 | Heater | 1 | 1
8 | Radio | 6 | 1
12 | Tyres | 6 | 1
13 | Rearview mirror | 8 | 1
14 | Door | 8 | 1
15 | Engine | 14 | 1
15 | Door | 3 | 1
I use function get_the_list(id = 0, owner_id = 1);
function get_the_list(id = 0, owner_id = 1) {
$query = "SELECT * FROM products WHERE main_id = $id AND owner_id = $owner_id";
while ($row = mysqli_fetch_array($result, MYSQLI_BOTH)) {
$list .= $row[product_title];
// select sub products from main_id
$list .= get_the_list($row[main_id], 1);
}
}
echo get_the_list(id = 0, owner_id = 1);
On this way I get the whole product list. Works well, no problems.
(1) i: 1 --- loop: 1, 1 - Volvo
i: 1 --- loop: 2, 2 - Rear lights
i: 2 --- loop: 3, 2 - Glasses
i: 3 --- loop: 4, 2 - Seats
i: 1 --- loop: 5, 3 - Radio
i: 1 --- loop: 6, 4 - Rear-view mirror
i: 2 --- loop: 7, 4 - Door
i: 1 --- loop: 8, 5 - Engine
i: 2 --- loop: 7, 3 - Tyres
i: 4 --- loop: 6, 2 - Heater
____________________________
(2) i: 2 --- loop: 1, 1 - Chevrolet
i: 1 --- loop: 2, 2 - Door
____________________________
(3) i: 3 --- loop: 1, 1 - Mercedes
____________________________
(4) i: 4 --- loop: 1, 1 - XX
____________________________
First (number) is main_id.
Second number is running $i++ in while.
Third number should be continuous/ongoing counter. BUT this breaks after level 5.
fourth number after comma is level.
I have to limit a select statement to only 8 products (incl. sub products).
So I will end, for example, with rear-view mirror on this image example.
It works.
But it works not after more than 8, because the counter breaks.
How can I limit the number of products that can be retrieved, what select statement should I choose? OR WHAT php workaround?
enter image description here
First of all consider moving to PDO instead of mysqli. Then set main_id nullable and add a foreign key for it as products.main_id -> products.id.
The modern way to handle this task is using CTE.
I'not using the owner_id here and I assume you only have two levels in your products tree:
WITH root_products AS (
SELECT
products.id,
products.main_id,
products.product_title,
products.id as sequence_route
FROM products WHERE main_id IS NULL OR main_id = 0
LIMIT 0, 20 -- this is where limit is set
)
SELECT * FROM (
SELECT * FROM root_products
UNION
SELECT
id,
main_id,
product_title,
CONCAT( main_id, '.', id ) sequence_route
FROM products
WHERE
main_id IN ( SELECT id FROM root_products )
) result
ORDER BY sequence_route
Note that this solution is using CTE. Check if your DB server version supports those. Tested with MySQL 8+
If you need some simplier queries, you will need two of those:
$query = "SELECT * FROM products WHERE main_id = 0 OR main_id IS NULL LIMIT 0, 20";
This will fetch 20 top-level products. And:
$query = "SELECT * FROM products WHERE main_id IN( " . implode(',', $root_ids_you_take_from_the_prev_query ) . " )";
will get the subproducts.
Upd:
Here's the query for unlimited depth. Its is still tied to a root product with id = 1, you might want to leave this in place for performance reasons or rework the WHERE clause to main_id IS NULL at the first SELECT to grab all the root entries. This query implements 'way #2'. It is still all about an ORDER BY. If you need to roll back to a layer-by-layer 'way #1', just drop the ORDER BY clause and corresponding rank/recursion_level columns from CTE.
-- This CTE will take the given root and
-- recursively grab all its descendants,
-- layer after layer
WITH RECURSIVE deeper_products AS (
SELECT
0 as recursion_level -- This is required for ranking
, CAST(id AS CHAR) as `rank` -- This should be 'CAST(id AS FLOAT)' but i'm 2 lazy to update my mysql installation
-- Server makes conversions internally just fine anyways
, CAST(id as CHAR) as sequence_route -- Just a visual helper, does nothing
, id
, main_id
, product_title
FROM products
WHERE id = 1 -- the root product id
UNION
SELECT
prev_layer.recursion_level + 1 as recursion_level
, prev_layer.rank + products.id / POWER( 100, prev_layer.recursion_level + 1 ) as `rank` -- I assume there'r less than 100 children there
, CONCAT( prev_layer.sequence_route, '.', products.id ) sequence_route
, products.id
, products.main_id
, products.product_title
FROM products
INNER JOIN deeper_products prev_layer
ON products.main_id = prev_layer.id
)
SELECT * FROM deeper_products
ORDER BY `rank` -- Remove this and the rank/recursion_level fields from CTE if you need a level-by-level ordering
LIMIT 0, 8 -- Your limit
Upd2:
Here's a PHP solution. I'm using PDO here (messing with mysqli is a pain). No recursion needed, so the limit is calculated pretty simple:
<?php
$pdo = new PDO("mysql:dbname=test;host=localhost", 'test', 'SECRET_PASSWORD');
function get_list( $dbh, $pid = null ){
$out = [];
$limit = 8;
// The regular query - items never have NULL as their main_id
$regular_query = "SELECT * FROM products WHERE main_id = ? LIMIT 0, $limit";
// The root query - items might have NULL as main_id
$root_query = (
is_null( $pid )
? "SELECT * FROM products WHERE main_id IS NULL LIMIT 0, $limit"
: $regular_query
);
// Results stack contains statements
// Every deeper level will add a new statement the top
$root_sth = $dbh->prepare( $root_query );
$root_sth->execute( [ $pid ] );
$results_stack = [ $root_sth ];
// Fill in the final result, loop untill the limit reached
// or while we still have records in DB
while( count( $out ) < $limit and count( $results_stack ) ){
// Take the topmost statement..
$sth = $results_stack[ count( $results_stack ) - 1 ];
// .. and grap the row
$row = $sth->fetch(PDO::FETCH_BOTH);
// The row is there, so we..
if( $row ){
// ..save it in the final result..
$out[] = $row;
// ..and add a new query returning children of a
// current item on the top of a stack
$regular_sth = $dbh->prepare( $regular_query );
$regular_sth->execute( [ $row['id'] ] );
$results_stack[] = $regular_sth;
}
// No row - we'r out of data with this query, remove the it from the stack
else{
array_pop( $results_stack );
}
}
return $out;
}
print_r( get_list( $pdo ) );
Upd3:
This builds the UL structure. I assume you've added the depth key for the items:
<?
$data = [
[ 'title' => 'Item 1', 'depth' => 1 ],
[ 'title' => 'Item 2', 'depth' => 1 ],
[ 'title' => 'Item 2.1', 'depth' => 2 ],
[ 'title' => 'Item 2.2', 'depth' => 2],
[ 'title' => 'Item 2.2.1', 'depth' => 3 ],
[ 'title' => 'Item 2.2.2', 'depth' => 3 ],
// [ 'title' => 'Item 3', 'depth' => 1 ],
];
?>
<ul>
<?
foreach( $data as $i => $row ){
?><li><?=$row['title']?><?
// If we even have a next level
if( !empty( $data[$i + 1]) ){
// If next item is on a level higher - open ul tag for it
if( $data[$i + 1]['depth'] > $row['depth'] ){?>
<ul>
<?}
// If next item is a level lower - close li/ul tags between the levels and close higher li
elseif( $data[$i + 1]['depth'] < $row['depth'] ){?>
<?=str_repeat('</li></ul>', $row['depth'] - $data[$i + 1]['depth'] )?>
</li>
<?}
// Next item is on the same level, just close self li and go on
else{?>
</li>
<?}
}
// This is the last item, close li/ul to the end
else{
print str_repeat("</li></ul>", $row['depth'] );
}
}?>

How to write a dynamic number of Eloquent queries in Laravel 5?

(This question is building on my other question here)
There are four tables in my database:
Users (columns id, dept_id)
Departments (columns id, deptStringName)
Absences (columns id, user_id, type_id)
Absence_types (columns id, stringName)
At the moment there are 10 rows in the Departments table and 12 rows in the Absence_types table.
I'm trying to get a query that outputs ten tables, one for each department, with the types of absences and their counts next to the name, e.g., for the IT department:
+-----------+---+
| Sickness | 4 |
| Paternity | 7 |
| Maternity | 3 |
| ... | 6 |
+-----------+---+
I know the query to get these results. However, I'm wondering what is the best practice from a Software Engineering standpoint: do I hard-code the values in the WHERE clause as I've done (dept.id = 1) in the query below?
SELECT COUNT(abs.id) as AbsenceCount , absence_types.description FROM Absences abs JOIN Users u on u.id = abs.user_id JOIN Departments dept on dept.id = u.dept_id JOIN Absence_types on Absence_types.id = abs.type_id WHERE dept.id = 1 group by dept.description
Or do I use some other way to get the ID of a department? I can't think of a way in which I could write a Laravel script that would know how many departments there are and then run one query each per department.
EDIT: Example result could be like the one below (ideally), with A, B, C Absence Types for X, Y, Z Departments
+---+---+---+---+
| | A | B | C |
+---+---+---+---+
| X | 4 | 8 | 5 |
| Y | 7 | 9 | 4 |
| Z | 5 | | |
+---+---+---+---+
Try this way..
Initialize the table by getting all departments and all absence types:
$departments = DB::table('Departments')->pluck('deptStringName');
$absenceTypes = DB::table('Absence_types')->pluck('stringName');
$row = [];
foreach ($absenceTypes as $absenceType) {
$row[$absenceType] = 0;
}
$table = [];
foreach ($departments as $department) {
$table[$department] = $row;
}
This should create a 2D array like:
[
'X' => [
'A' => 0,
'B' => 0,
'C' => 0,
],
// ...
]
You can use NULL or an empty string instead of 0 if you like.
Now modify your query a bit. You need to group by departments and absence types.
$data = DB::select('
SELECT
d.deptStringName as department,
t.stringName as absenceType,
COUNT(a.id) as absenceCount
FROM Absences a
JOIN Users u ON u.id = a.user_id
JOIN Departments d ON d.id = u.dept_id
JOIN Absence_types t ON t.id = a.type_id
GROUP BY d.deptStringName, t.stringName
');
And fill the $table with values fron the query:
foreach ($data as $row) {
$table[$row->department][$row->absenceType] = $row->absenceCount
}
Now the $table shlould look like
[
'X' => [
'A' => 4,
'B' => 8,
'C' => 5,
],
'Y' => [
'A' => 7,
'B' => 9,
'C' => 4,
],
'Z' => [
'A' => 5,
'B' => 0,
'C' => 0,
],
]

How properly SELECT several one-to-many rows?

We have 3 tables.
1) Order
PK
order_id
2) Order_Products
PK | FK | Column | Column
order_product_id | order_id | product_id | name | quantity
3) Order_Product_Components
PK | FK | Column | Column | Column
order_product_component_id | order_product_id | stock_id | name | quantity
Tables 2 and 3 have one-to-many relationship.
So if we want to get an array of order products with their components what is the best query we can use?
Finally the result should be this format (if we have just 1 product with 2 components):
$products = array(
array(
' 'product_id' => x,
'name' => x,
'quantity' => x,
'stocks' => array(
array(
'stock_id' => x,
'name' => x,
'quantity' => x),
array(
'stock_id' => x,
'name' => x,
'quantity' => x)));
Since order is a reserved word I have changed the order table name to 'orders'. I am using join to get the expected results. I think group by would not be as useful to get the result the way you want it.
$sql= "select * from orders o join order_products op on o.order_id=op.order_id";
$stmt = $db->prepare($sql);
$stmt->execute();
$orders =$stmt->fetchAll();
foreach($orders as $o){
$sql= "select stock_id,name,quantity from order_products op
join order_product_components opc on opc.order_product_id=op.order_product_id where op.order_product_id=:orderprodid";
$stmt = $db->prepare($sql);
$stmt->execute(array('orderprodid'=>$o['order_product_id']));
$comps =$stmt->fetchAll();
$o['stocks']=$comps;
}
You may not want to do a loop of queries, which can be very expensive.
Just do a query joining all tables and order by product id,product name, ... stock id, stock name,... and loop through the results and build your array in one pass.

Getting relational data into arrays

I have a animal table:
id | name
1 cat
2 dog
I have a breeds table:
id | breed | color
1 siamese white
2 tabby mixed
3 golden retriever yellow
I have a animal to breeds table:
id | animal_id | breed_id
1 1 1
2 1 2
3 2 3
I want to get out each animal and all of its breeds/colors into an array which I'll then pass to JS.
So the data should look like:
array('animal' => 'cat', 'details' => array(array('breed' => 'siamese', 'color' => 'white'), array('breed' => 'tabby', 'color' => 'mixed')) 'animal' => 'dog...etc);
I'm not sure on the query though? I need to get data for all animals.
This should do the trick:
$query = "SELECT a.name, b.breed, b.colour
FROM animals a
LEFT JOIN animaltobreeds ab on a.id = ab.animal_id
LEFT JOIN breeds b on b.id = ab.breed_id";
$results = $mysqli->query($query);
$data = array(); // We'll build our results into here.
if($results->num_rows) {
while($row = $results->fetch_assoc()) {
if( ! isset($data[$row['name']]) $data[$row['name']] = array();
$data[$row['name']][] = array(
'breed' => $row['breed'],
'colour' => $row['colour']
);
}
}
Then if you want to output it for Javascript you can turn it into JSON by using json_encode($data), then import it into Javascript.
You may notice I've done a slightly different structure to the one you requested, this one will look like this:
array('cats' => array('breed => 'siamese', 'colour' => 'white'), array('breed' => 'tabby', 'color' => 'mixed'));
I think it's easier to work with, but it should be easy enough to adjust if you really need it to be what you want it to be.

Trying to group up results of a SQL multi table query

alright best way for me to explain is by giving an example
Table 1
ID | name | class
-------------------------
1 | first name | a
2 | second name | b
Table 2
ID | name_id | meta_key | meta_value |
-------------------------------------------------------
1 | 1 | image | someImage.jpg |
2 | 1 | description | very long description |
3 | 2 | image | someImage_2.jpg |
4 | 2 | description | very long description 2 |
I am trying to select name and class from Table 1, and also all the records from Table 2 that has the same name_id as ID (in Table 1).
Here is the query i have now:
SELECT
table1.name,
table1.class,
table2.name_id,
table2.meta_key,
table2.meta_value
FROM
table1
LEFT JOIN
table2 ON ( table1.ID = table2.name_id )
WHERE
table1.ID = '2'
This works and return an array with as many elements as the Table 2 entries that has name_id = 2
Is there a way to return an array with 1 the result from first table, and another array with results from the second table ?
Update:
The ideal results will be:
$result['table1'][0] = array('name' => 'second name',
'class' => 'b')
$result['table2'][0] = array('name_id' => 2,
'meta_key' => 'image',
'meta_value' => 'someImage_2.jpg')
$result['table2'][1] = array('name_id' => 2,
'meta_key' => 'description',
'meta_value' => 'very long description 2')
hopes that makes sense.
I'd split the result array AFTER fetching it from the database...
//Assuming your result-set is assigned to $result:
$table1_keys = array('id', 'name', 'class');
$table2_keys = array('id', 'name_id', 'meta_key', 'meta_value');
$table1_results = array();
$table2_results = array();
foreach($result as $key => $val) {
if(in_array($key, $table1_keys)) {
$table1_results[] = $val;
}
if(in_array($key, $table2_keys)) {
$table2_results[] = $val;
}
}
After this, your result-set should be split into two arrays: $table1_results and $table2_results.
Hope this helps you.
I don't believe there's a way to do it in SQL but I'd label each table so it can be done generically in PHP.
SELECT
':table1',
table1.name,
table1.class,
':table2',
table2.name_id,
table2.meta_key,
table2.meta_value
FROM ...
Although personally I'd just do this.
SELECT 'table1', table1.*, ':table2', table2.*
FROM ...
That makes it easier to duplicate entire rows between client and server and reduces maintenance when you decide to add a new field.
However in your query you are duplicating table1 for each row returned so perhaps this would be better.
SELECT
table1.*,
table2a.meta_value AS image,
table2b.meta_value AS description
FROM
table1
LEFT JOIN
table2 AS table2a ON ( table1.ID = table2a.name_id AND table2a.meta_key = 'image' )
LEFT JOIN
table2 AS table2b ON ( table1.ID = table2b.name_id AND table2b.meta_key = 'description' )
WHERE
table1.ID = '2'

Categories