Zend 2 subquery columns - mysql

I want to create a SQL(MySQL) query in Zend Framework 2 like:
SELECT a.id,
a.name,
a.age,
(SELECT MAX(score)
FROM scores AS s
WHERE s.user_id = a.id) AS max_score,
(SELECT SUM(time)
FROM games_played_time AS gpt
WHERE gpt.user_id = a.id) AS time_played
FROM users AS a
ORDER BY last_visited DESC
LIMIT 0, 100
Mind that this is an artificial example of existing query.
I tried creating sub-queries and then creating main select query where when I use:
$select->columns(
array(
'id',
'name',
'age',
'max_score' => new Expression('?', array($sub1),
'time_played' => new Expression('?', array($sub2)
)
I also tried using:
$subquery = new \Zend\Db\Sql\Expression("({$sub->getSqlString()})")
And even lambda functions like suggested here: http://circlical.com/blog/2014/1/27/zend-framework-2-subqueries-subselect-and-table-gateway
Still no luck because all the time I keep getting errors like:
No data supplied for parameters in prepared statement
And when I succeed in making the query work, it ends up that column contains the text of sub-queries. It starts to look that it is not possible to make multiple expressions in columns method. Any ideas?
SOLVED:
I rewrote query by query as #Tim Klever proposed. Everythin worked except one query. It turns out there is some kind of issue when using limit in subquery and in main query. In my case one of the subqueries returns multiple rows, so I ussed limit(1) to force return of a single value. But using that turned out to produce error:
No data supplied for parameters in prepared statement
I changed the query to use MAX instead of limit and now it works. Later will try to debug why this is happening.. Thank you!

The following worked for me to produce the query you listed
$maxScoreSelect = new Select();
$maxScoreSelect->from(array('s' => 'scores'));
$maxScoreSelect->columns(array(new Expression('MAX(score)')));
$maxScoreSelect->where->addPredicates('s.user_id = a.id');
$sumTimeSelect = new Select();
$sumTimeSelect->from(array('gpt' => 'games_played_time'));
$sumTimeSelect->columns(array(new Expression('SUM(time)')));
$sumTimeSelect->where->addPredicates('gpt.user_id = a.id');
$select = new Select();
$select->from(array('a' => 'users'));
$select->columns(array(
'id',
'name',
'age',
'max_score' => new Expression('?', array($maxScoreSelect)),
'time_played' => new Expression('?', array($sumTimeSelect))
));
$select->order('last_visited DESC');
$select->limit(100);
$select->offset(0);

Related

How to count all records if use alias in select query?

I use Sphinx with Yii2 and need to query with filter by jSON field.
$query = new \yii\sphinx\Query();
$query->from('announcements');
$query->addSelect("*");
$query->addSelect(new Expression("IN(filters['color'], 'blue', 'red', 'green') AS f_color"));
$query->where("is_active = 1");
$query->andWhere("f_color = 1");
$announces = $query->all();
There is jSON field filters in my Sphinx index. For example:
[filters] => {"brand":"Toyota","model":"Prius","color":"red","price":"12000"... etc]
It works OK. But now I need to make a pagination... and there is a problem when I try to count records before $query->all()
$count = $query->count(); // Return error "no such filter attribute 'f_color'"
Generated query was:
SELECT COUNT(*) FROM announcements WHERE ( is_active = 1 ) AND ( f_color = 1 )
count() by default replaces the select part with * and this is where your alias is defined hence the error.
There are different ways to achieve it like:
use ActiveDataProvider like described here,
use META information like described here
Since you want to make a pagination I would go with the first example.

Query matching() causing duplicate rows and distinct() not working

I am trying to filter one table Payments by a field on the associated table Invoices.
Using the function matching() on the query object filters correctly but causes duplicate rows. It seemed like the solution was using distinct(), but calling distinct(Payments.id) results in an invalid query. I'm doing the following in a controller action.
$conditions = [
'Payments.is_deleted =' => false
];
$args = [
'conditions' => $conditions,
'contain' => ['Invoices', 'Invoices.Clients'],
];
$payments = $this->Payments->find('all', $args);
if($issuer) {
// This causes duplicate rows
$payments->matching('Invoices', function ($q) use ($issuer) {
return $q->where(['Invoices.issuer_id' => $issuer['id']]);
});
// $payments->distinct('Payments.id'); // Causes a mysql error
}
Am I correct in thinking that distinct() is what I need, and if so any idea what's missing to make it work?
I'm getting the following mysql error when uncommenting the line above:
Error: SQLSTATE[42000]: Syntax error or access violation: 1055 Expression #8 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'InvoicesPayments.id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
Full query:
SELECT
PAYMENTS.ID AS `PAYMENTS__ID`,
PAYMENTS.CREATED AS `PAYMENTS__CREATED`,
PAYMENTS.MODIFIED AS `PAYMENTS__MODIFIED`,
PAYMENTS.DATE_REGISTERED AS `PAYMENTS__DATE_REGISTERED`,
PAYMENTS.USER_ID AS `PAYMENTS__USER_ID`,
PAYMENTS.AMOUNT AS `PAYMENTS__AMOUNT`,
PAYMENTS.IS_DELETED AS `PAYMENTS__IS_DELETED`,
INVOICESPAYMENTS.ID AS `INVOICESPAYMENTS__ID`,
INVOICESPAYMENTS.INVOICE_ID AS `INVOICESPAYMENTS__INVOICE_ID`,
INVOICESPAYMENTS.PAYMENT_ID AS `INVOICESPAYMENTS__PAYMENT_ID`,
INVOICESPAYMENTS.PART_AMOUNT AS `INVOICESPAYMENTS__PART_AMOUNT`,
INVOICES.ID AS `INVOICES__ID`,
INVOICES.CREATED AS `INVOICES__CREATED`,
INVOICES.MODIFIED AS `INVOICES__MODIFIED`,
INVOICES.IS_PAID AS `INVOICES__IS_PAID`,
INVOICES.IS_DELETED AS `INVOICES__IS_DELETED`,
INVOICES.CLIENT_ID AS `INVOICES__CLIENT_ID`,
INVOICES.ISSUER_ID AS `INVOICES__ISSUER_ID`,
INVOICES.NUMBER AS `INVOICES__NUMBER`,
INVOICES.SUBTOTAL AS `INVOICES__SUBTOTAL`,
INVOICES.TOTAL AS `INVOICES__TOTAL`,
INVOICES.DATE_REGISTERED AS `INVOICES__DATE_REGISTERED`,
INVOICES.CURRENCY AS `INVOICES__CURRENCY`,
INVOICES.RECEIVER_NAME AS `INVOICES__RECEIVER_NAME`,
INVOICES.RECEIVER_RFC AS `INVOICES__RECEIVER_RFC`,
INVOICES.EMAIL_SENDER AS `INVOICES__EMAIL_SENDER`,
INVOICES.PDF_PATH AS `INVOICES__PDF_PATH`
FROM
PAYMENTS PAYMENTS
INNER JOIN
INVOICES_PAYMENTS INVOICESPAYMENTS
ON PAYMENTS.ID = (
INVOICESPAYMENTS.PAYMENT_ID
)
INNER JOIN
INVOICES INVOICES
ON (
INVOICES.ISSUER_ID = :C0
AND INVOICES.ID = (
INVOICESPAYMENTS.INVOICE_ID
)
)
WHERE
(
PAYMENTS.IS_DELETED = :C1
AND PAYMENTS.DATE_REGISTERED >= :C2
AND PAYMENTS.DATE_REGISTERED <= :C3
)
GROUP BY
PAYMENT_ID
ORDER BY
PAYMENTS.DATE_REGISTERED ASC
That behavior is expected, as matching will use an INNER join, and yes, grouping is how you avoid duplicates:
As this function will create an INNER JOIN, you might want to consider calling distinct on the find query as you might get duplicate rows if your conditions don’t exclude them already. This might be the case, for example, when the same users comments more than once on a single article.
Cookbook > Database Access & ORM > Query Builder > Loading Associations > Filtering by Associated Data
As the error message states, your MySQL server is configured to use the strict only_full_group_by mode, where your query is invalid. You can either disable that strict mode as mentioned by Akash prajapati (which can come with its own problems, as MySQL is then allowed to pretty much pick values of a group at random), or you could change how you query things in order to conform to the strict mode.
In your case where you need to group on the primary key, you could simply switch to using innerJoinWith() instead, unlike matching() this will not add any fields of that association to the SELECT list, and things should be fine in strict mode, as everything else is functionally dependent:
In cases where you would group on a key that would break functional dependency detection, one way to solve that could for example be to use a subquery for filtering, one that only selects that key, something along the lines of this:
$conditions = [
'Payments.is_deleted =' => false
];
$payments = $this->Payments
->find()
->contain(['Invoices.Clients']);
if($issuer) {
$matcherQuery = $this->Payments
->find()
->select(['Payments.some_other_field'])
->where($conditions)
->matching('Invoices', function ($q) use ($issuer) {
return $q->where(['Invoices.issuer_id' => $issuer['id']]);
})
->distinct('Payments.some_other_field');
$payments->where([
'Payments.some_other_field IN' => $matcherQuery
]);
} else {
$payments->where($conditions);
}
This will result in a query similar to this, where the outer query can then select all the fields you want:
SELECT
...
FROM
payments
WHERE
payments.some_other_field IN (
SELECT
payments.some_other_field
FROM
payments
INNER JOIN
invoices_payments ON
payments.id = invoices_payments.payment_id
INNER JOIN
invoices ON
invoices.issuer_id = ...
AND
invoices.id = invoices_payments.invoice_id
WHERE
payments.is_deleted = ...
GROUP BY
payments.some_other_field
)
The problem with sql_mode value in mysql so you need to set the sql_mode value as blank and then you can try and working fine for you.
SET GLOBAL sql_mode=(SELECT REPLACE(##sql_mode,'ONLY_FULL_GROUP_BY',''));
Please let me know still anything else.
I had the same issue, but was too afraid to set the sql_mode as mentioned by #Akash and also too much in a hurry to restructure the query. So I decided to use the inherited Collection method indexBy()
https://book.cakephp.org/4/en/core-libraries/collections.html#Cake\Collection\Collection::indexBy
$resultSetFromYourPaymentsQuery = $resultSetFromYourPaymentsQuery->indexBy('id');
It worked like a charm and it is DB independent.
EDIT: After some more tinkering, this might not be practical for all use cases. Replacing matching with innerJoinWith as proposed in the accepted answer will probably solve it in more generalized manner.

Using limit and offset in subquery in Zend framework 2

I have two tables: users and usermeta. The table usermetahas 4 columns: id, user_id, meta_key and meta_value. A user can have many meta data.
I want to get the groups of the first 10 users. So I create a SQL query in Zend framework 2:
$coreSelect = $sql->select()
->from('users')
->limit(10)
->offset(0);
$select = $sql->select()
->from(array('u' => $coreSelect))
->join('usermeta',
'u.user_id = usermeta.user_id',
array(
'meta_key' => 'meta_key',
'meta_value' => 'meta_value',
),
Select::JOIN_LEFT
);
When execute this query, it show up a SQL syntax error. The SQL query string manipulated by Zend framework is like this:
SELECT u.*
FROM (
SELECT * FROM users LIMIT '10' OFFSET '0') AS u
...
It means that Zend framework has added quotes into offset and limit value and it makes the syntax error.
Can anyone please help me to resolve this problem?
This should help you:
http://www.maltblue.com/tutorial/zend-db-sql-creating-joins-and-unions-with-ease
Something that interests you, towards the end, but read all of it is useful.

How could I create a sub-query in cakePHP?

How could I create a sub-query in cakePHP with find method? For example:
SELECT *, (SELECT COUNT(*) FROM table2 WHERE table2.field1 = table1.id) AS count
FROM table1
WHERE table1.field1 = 'value'
!!! table2.field1 = table1.id !!!
Just for addition, you can build subquery using Cake's ORM. It will be more easy. Please read CakePHP retrieving data doc
In general you can use buildStatement() method of DataSource object. You can save all logic, including pagination etc.
You can do this one of two ways:
1. Use $this->Model->query(...)
This allows you execute SQL directly using the query you posted above but be aware that it can make your application quite brittle if the database schema were to change. Probably the fastest.(Documentation)
Example
$this->Model1->query("SELECT * FROM model;");
2. Separate the Calls
This is probably not as fast as option 1 but it does give you the ability to break your query down into a number of steps. You're also unlikely to get SQL injection which is potential risk with option 1.
Example
$model1s = $this->Model1->find
(
'all',
array
(
'conditions' => array('Model1.field1' => 'value')
)
);
foreach($model1s as $model1)
{
$model1['model2_count'] = $this->Model2->find
(
'count',
array
(
'conditions' => array
(
'Model2.field1' => $model1['id']
)
)
);
}

How to write a select count group by SQL query in LINQ?

I have this query which works but when I try to write the equivalent in LINQ I get the incorrect SQL produced.
My query is:
SELECT COUNT(*)
FROM tableName
GROUP BY ColumnId
I've tried writing it as:
tableName.GroupBy(x => x.ColumnId).Count()
But looking in LINQPad it is producing the SQL:
SELECT COUNT(*) AS [value]
FROM (
SELECT NULL AS [EMPTY]
FROM [tableName] AS [t0]
GROUP BY [t0].[ColumnId]
) AS [t1]
What am I doing wrong? Thanks!
Your LINQ query is counting the number of groups but your SQL query is producing the counts by group. You want
var counts = tableName.GroupBy(x => x.ColumnId)
.Select(g => new { g.Key, Count = g.Count() });
to get the counts by group.
Note that if you want exactly the same SQL you want
var counts = tableName.GroupBy(x => x.ColumnId)
.Select(g => g.Count());
The first example above should be a little more useful as it gives the ids of each group as well.
Try tableName.GroupBy(x => x.ColumnId).Select(x => x.Count())