Get average from nested eloquent relationship - mysql

I would like to calculate average from nested relationship between eloquent models. So, let's say, I have 3 tables called programs, activities and statistics.
For simplicity sake, I will try to minimize the structure as follows:
program table:
-------------
| id | name |
-------------
| 1 | Foo |
| 2 | Bar |
-------------
activities table:
-----------------------------------
| id | program_id | name |
-----------------------------------
| 1 | 1 | Foo 1 |
| 2 | 1 | Foo 2 |
| 3 | 1 | Foo 3 |
| 4 | 2 | Bar 1 |
| 5 | 2 | Bar 2 |
-----------------------------------
statistics table:
-----------------------------------
| id | activity_id | type | score |
-----------------------------------
| 1 | 1 | A | 25 |
| 2 | 1 | B | 20 |
| 3 | 1 | A | 22 |
| 4 | 2 | A | 27 |
| 5 | 2 | B | 24 |
| 6 | 3 | A | 23 |
-----------------------------------
Now, what I want to get is the average of score of a program with specific type of statistic. I defined relationship in models, and tried following code, but no avail:
$program = Program::find(1);
$avg = $program->activities->where('statistics.type', 'A')->avg('statistics.value');
$avg always 0 or null if there is no activities in program, even without where clause.
i'm sure that i defined the relationship correctly because $program->activities returns a sets of activities and $activity-> statistics return a sets of statistics as well.
Any ideas?

You can use whereHas() like this:
Statistics::whereHas('activity', function ($q) use($programId) {
$q->where('program_id', $programId);
})
->where('type', 'A')
->avg('score');
Make sure you've defined activity relationship which should be "statistics belongsTo() activity".

Related

Precalculate numbers of records for each possible combination

I have a mySQL database table containing cellphones information like this:
ID Brand Model Price Type Size
==== ===== ===== ===== ====== ====
1 Apple A71 3128 A 40
2 Samsung B7C 3128 B 20
3 Apple ZX5 3128 A 30
4 Huawei Q32 2574 B 40
5 Apple A21 2574 A 25
6 Apple A71 3369 A 30
7 Samsung A71 7413 C 40
Now I want to create another table, that would contain counts for every possible combination of the parameters.
Params Count
============================================== =======
ALL 1000000
Brand(Apple) 20000
Brand(Apple,Samsung) 40000
Brand(Apple),Model(A71) 7100
Brand(Apple),Type(A) 6000
Brand(Apple),Model(A71,B7C),Type(A,B) 7
Model(A71) 12514
Model(A71,B7C) 26584
Model(A71),Type(A) 6521
Model(A71),Type(A,B) 8958
Model(A71),Type(A,B),Size(40) 85
And so on for every possible combination. I was thinking about creating a stored procedure (that i would execute periodically), that would perform queries with every existing condition like that, but I am a little stuck on how exactly should it look like. Or is there a better way how to do this?
Edit: the reason why I want to store information like this is to be able to show number of results in filter in client application, like in the picture.
I would like to create index on the Params column to be able to get the Count number for given hash instantly, improving performance.
I also tried querying and caching the values dynamically, but I want to try this approach as well, so I can compare which one is more effective.
This is how I am calculating the counts now:
SELECT COUNT(*) FROM products;
SELECT COUNT(*) FROM products WHERE Brand IN ('Apple');
SELECT COUNT(*) FROM products WHERE Brand IN ('Apple', 'Samsung');
SELECT COUNT(*) FROM products WHERE Brand IN ('Apple') AND Model IN ('A71');
etc.
You can use a ROLLUP for this.
SELECT
model, type, size, COUNT(*)
FROM mytab
GROUP BY 1, 2, 3
WITH ROLLUP
With your sample data, we get the following:
| model | type | size | COUNT(*) |
| ----- | ---- | ---- | -------- |
| A21 | A | 25 | 1 |
| A21 | A | | 1 |
| A21 | | | 1 |
| A71 | A | 30 | 1 |
| A71 | A | 40 | 1 |
| A71 | A | | 2 |
| A71 | C | 40 | 1 |
| A71 | C | | 1 |
| A71 | | | 3 |
| B7C | B | 20 | 1 |
| B7C | B | | 1 |
| B7C | | | 1 |
| Q32 | B | 40 | 1 |
| Q32 | B | | 1 |
| Q32 | | | 1 |
| ZX5 | A | 30 | 1 |
| ZX5 | A | | 1 |
| ZX5 | | | 1 |
| | | | 7 |
The subtotals are present in the rows with null values in different columns, and the total is the last row where all group by columns are null.

Creating a view in MySQL with columns created from row data in a table?

I've got a MySQL database containing three tables. The database contains information about various electrical and mechanical components. It has three tables.
Tables:
componentSource - contains information about where the information in the database was sourced from.
component - contains part number information, description, etc. Multiple entries will refer to a single entry in the componentSource table as its source (Each source file describes multiple components).
componentParams - contains parametric information about the components. Multiple parameter entries will refer to a single entry the component table (each component has multiple parameters).
See simplified example tables...
Database Tables and Relationships:
+-------------------------------+
| Table: componentSource |
+-------------------------------+
| compSrcID* | sourceFile |
+-------------------------------+
| 1 | comp1.txt |
| 2 | comp2.txt |
| 3 | comp3.txt |
+-------------------------------+
^
|
+---------------------------------------------------+
( many to one reference) |
^
^
+---------------------------------------------------------------+
| Table: component |
+---------------------------------------------------------------+
| compID* | partNum | mfrPartNum | mfr | compSrcID |
+---------------------------------------------------------------+
| 1 | 1234 | ABCD | BrandA | 1 |
| 2 | 2345 | BCDE | BrandB | 1 |
| 3 | 3456 | CDEF | BrandC | 3 |
| 4 | 4567 | DEFG | BrandD | 2 |
+---------------------------------------------------------------+
^
|
+---------------+ (many to one reference)
|
^
^
+-------------------------------------------------------+
| Table: componentParams |
+-------------------------------------------------------+
| compParamID* | compID | paramName | paramValue |
+-------------------------------------------------------+
| 1 | 1 | ParamA | 50 |
| 2 | 1 | ParamB | 123 |
| 3 | 1 | ParamC | 10% |
| 4 | 1 | ParamD | 0.5 |
| 5 | 1 | ParamE | Active |
| 6 | 2 | ParamA | 25 |
| 7 | 2 | ParamB | 10K |
| 8 | 2 | ParamC | 5% |
| 9 | 2 | ParamD | 0.25 |
| 10 | 2 | ParamE | Proto |
| 11 | 3 | ParamA | 53.6 |
| 12 | 3 | ParamE | Active |
| 13 | 4 | ParamY | 123-56 |
| 14 | 4 | ParamZ | True |
+-------------------------------------------------------+
I would like to create a view of the database that merges information from the three tables. I would like to have a row for each line in the component table that merges the relevant lines from the componentSource table, and all of the relevant parameters out of the componentParams table.
See example view...
Database View:
+-------------------------------------------------------------------------------------------------------------------------------------------------------+
| View: componentView |
+-------------------------------------------------------------------------------------------------------------------------------------------------------+
| compID* | partNum | mfrPartNum | mfr | SourceFile | ParamA | ParamB | ParamC | ParamD | ParamE | ParamY | ParamZ |
| 1 | 1234 | ABCD | BrandA | comp1.txt | 50 | 123 | 10% | 0.5 | Active | | |
| 2 | 2345 | BCDE | BrandB | comp1.txt | 25 | 10K | 5% | 0.25 | Proto | | |
| 3 | 3456 | CDEF | BrandC | comp3.txt | 53.6 | | | | Active | | |
| 4 | 4567 | DEFG | BrandD | comp2.txt | | | | | | 123-56 | True |
+-------------------------------------------------------------------------------------------------------------------------------------------------------+
Since I want a line in the view for each component in the component table, I think merging the info from the componentSource table is fairly straight forward with a join, but the tricky part is creating columns in the view that correspond to the value in componentParam.paramName column. Seems like this requires some recursion to read all parameters associated with a component. Also note that not all components have all the same parameters in the parameter table, so the values for the parameters not used by a component would be null.
An alternative to creating a view, if that can't be done, would be to build another database table.
My SQL skills are super rusty, and were probably not up to this task when they were fresh.
Is it possible to create a view that creates columns that are based on row data (paramName) in a table? Could you show an example?
If not, can a table be built that does the same? Again, could you show an example?
Many thanks.
Conditional aggregation can do the pivoting for you
SELECT cp.compID,
ct.partNum,
ct.mfrPartNum,
ct.mfr,
cs.SourceFile,
MAX(CASE WHEN cp.paramName = 'ParamA' THEN cp.ParamValue END) as ParamA,
MAX(CASE WHEN cp.paramName = 'ParamB' THEN cp.ParamValue END) as ParamB,
MAX(CASE WHEN cp.paramName = 'ParamC' THEN cp.ParamValue END) as ParamC,
MAX(CASE WHEN cp.paramName = 'ParamD' THEN cp.ParamValue END) as ParamD,
MAX(CASE WHEN cp.paramName = 'ParamE' THEN cp.ParamValue END) as ParamE,
MAX(CASE WHEN cp.paramName = 'ParamY' THEN cp.ParamValue END) as ParamY,
MAX(CASE WHEN cp.paramName = 'ParamZ' THEN cp.ParamValue END) as ParamZ
FROM componentParameters cp
JOIN component ct ON cp.compId = ct.compId
JOIN componentSource cs ON cs.compSrcID = ct.compSrcID
GROUP BY cp.compID,
ct.partNum,
ct.mfrPartNum,
ct.mfr,
cs.SourceFile
It is also possible to use subqueries for this, however, I guess this should do the job better.

Find unique/duplicated rows from has and belongs to many association

I have following DB structure:
Table cars:
+----+-----------------------+
| id | few other columns.... |
+----+-----------------------+
| 1 | ... |
| 2 | ... |
| 3 | ... |
+----+-----------------------+
Table properties:
+----+-------+
| id | name |
+----+-------+
| 1 | title |
| 2 | type |
| 3 | brand |
| 4 | color |
+----+-------+
Table cars_properties:
+----+--------+-------------+------------+
| id | car_id | property_id | txt |
+----+--------+-------------+------------+
| 1 | 1 | 1 | Volvo V70 |
| 2 | 1 | 2 | personal |
| 3 | 1 | 3 | Volvo |
| 4 | 1 | 4 | white |
| 5 | 2 | 1 | Volvo VV |
| 6 | 2 | 2 | personal |
| 7 | 2 | 3 | Volvo |
| 8 | 2 | 4 | blue |
| 9 | 3 | 1 | Volvo XXL |
| 10 | 3 | 2 | truck |
| 11 | 3 | 3 | Volvo |
| 12 | 3 | 4 | white |
+----+--------+-------------+------------+
I would like to get all cars that have unique/duplicated values in one or many properties. Currently I'm using this SQL pattern to get duplicates for car type and brand:
SELECT cars.id FROM cars
LEFT JOIN cars_properties AS cp_0 ON cp_0.car_id = cars.id AND cp_0.property_id = 2 # => type
LEFT JOIN cars_properties AS cp_1 ON cp_1.car_id = cars.id AND cp_1.property_id = 3 # => brand
INNER JOIN (
SELECT cp_0.txt AS type_txt, cp_1.txt AS brand_txt FROM cars
LEFT JOIN cars_properties AS cp_0 ON cp_0.car_id = cars.id AND cp_0.property_id = 2
LEFT JOIN cars_properties AS cp_1 ON cp_1.car_id = cars.id AND cp_1.property_id = 3
GROUP BY cp_0.txt, cp_1.txt
HAVING COUNT(cars.id) > 1
) dupes ON cp_0.txt=dupes.type_txt AND cp_1.txt=dupes.brand_txt;
And expected result is:
+----+
| id |
+----+
| 1 |
| 2 |
+----+
Explanation: Both cars with id = 1 and 2 has type and brand that is present in more than one car (multiple times).
As for unique cars, I'm just altering: HAVING COUNT(cars.id) = 1 and I want to find all rows where the combination of properties is present only in one car (once).
It works fine, but it's extremely slow with more than 2 properties I want to check.
I cannot change the DB structure, and I'm not sure how to optimize the query, or if there are better ways of achieving this.
It feels like I would need to implement counter table, where each property id and value (txt) would also store corresponding number of occurrences in cars, and update this counter on every insert/update/delete... But I still hope there is some better SQL, that could help. Do you know some? Any advice greatly appreciated, thanks!
PS: I tried to create fiddle for it, but after I build schema I cannot run any SQL on it. To quickly setup DB with data, you can check SQL Fiddle

MySQL - Join tables and convert rows to columns

I have two tables similar to these (t_stamp would normally be a DATETIME, abbreviated here for clarity):
datapoints
+------+---------+----+---------+
| ndx | value | ID | t_stamp |
+------+---------+----+---------+
| 1 | 503.42 | 1 | 3/1/15 |
| 2 | 17.81 | 2 | 3/1/15 |
| 4 | 498.21 | 1 | 3/2/15 |
| 4 | 19.51 | 2 | 3/2/15 |
+------+---------+----+---------+
parameters
+------+----+---------------+-------+
| ndx | ID | description | unit |
+------+----+---------------+-------+
| 1 | 1 | wetwell level | ft |
| 2 | 2 | effluent flow | MGD |
+------+----+---------------+-------+
I'm looking to combine them so that the descriptions become column headers and list the values in order of time stamp, end result looking something like this:
new table
+---------+---------------+---------------+
| t_stamp | wetwell level | effluent flow |
+---------+---------------+---------------+
| 3/1/15 | 503.42 | 17.81 |
| 3/2/15 | 498.21 | 19.51 |
+---------+---------------+---------------+
Bearing in mind, I have considerably more rows in each table so I'm looking for something dynamic. It could be query or stored procedure based. Thank you for any help!

Subtract values from line above the current line in MySQL

I've the following table:
| id | Name | Date of Birth | Date of Death | Result |
| 1 | John | 3546565 | 3548987 | |
| 2 | Mary | 5233654 | 5265458 | |
| 3 | Lewis| 6546876 | 6548752 | |
| 4 | Mark | 6546546 | 6767767 | |
| 5 | Steve| 6546877 | 6548798 | |
And I need to do this for the whole table:
Result = 1, if( current_row(Date of Birth) - row_above_current_row(Date of Death))>X else 0
To make things easier, I guess, I created the same table above but with 2 extra id fields: id_minus_one and id_plus_one
Like this:
| id | id_minus_one | id_plus_one |Name | Date_of_Birth | Date_of_Death | Result |
| 1 | 0 | 2 |John | 3546565 | 3548987 | |
| 2 | 1 | 3 |Mary | 5233654 | 5265458 | |
| 3 | 2 | 4 |Lewis| 6546876 | 6548752 | |
| 4 | 3 | 5 |Mark | 6546546 | 6767767 | |
| 5 | 4 | 6 |Steve| 6546877 | 6548798 | |
So my approach would be something like (in pseudo code):
for id=1, ignore result. (Because there is no row above)
for id=2, Result = 1 if( (Where id=2).Date_of_Birth - (where id_minus_one=id-1).Date_of_Death )>X else 0
for id=3, Result = 1 if( (Where id=3).Date_of_Birth - (where id_minus_one=id-1).Date_of_Death)>X else 0
and so on for the whole table...
Just ignore id_plus_one if there is no need for it, I'll use it later for the same thing. So, if I manage to do this for id_minus_one I'll manage for id_plus_one as they are the same algorithm.
My question is how to pass that pseudo code into SQL code, I can't find a way to relate both ids in just one select.
Thank you!
As you describe this, it is just a self join with some logic on the select:
select t.*,
((t.date_of_birth - tprev.date_of_death) > x) as flag
from t left outer join
t tprev
on t.id_minus_one = tprev.id