How to SELECT * WHERE JSON keys contain ANY/IN (array of values) - json

I am dealing with a Symfony "workflow" "Marker" and Postgres. (and Doctrine)
in the db, the column "status" contains JSON data like this
{"needs_address":1,"needs_contacts":1,"needs_education":1,"needs_health_and_social":1,"has_profile_photo":1,"has_letter":1}
I figured out how to query like this
SELECT id, profile_status, beneficiary_code
FROM public.beneficiary_profile
WHERE profile_status->>'needs_address' = '1'
How can I query for a list of status' like
('needs_education','needs_contacts','needs_address')
without writing it all out like
WHERE profile_status->>'needs_address' = '1'
OR profile_status->>'needs_contacts' = '1'
OR profile_status->>'needs_education' = '1'
I figure there must be a way with JSON functions and maybe IN() or ANY()

You will need to unnest the elements in order to be able to use an IN clause, something like:
select *
from public.beneficiary_profile p
where exists (select *
from jsonb_each_text(p.status) as x(ky,val)
where x.ky in ('needs_address', 'needs_contact', 'needs_education')
and x.val = '1');

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.

MySQL select where JSON field property has value

How to write a basic MySQL query that has a WHERE on a property within a JSON data-type field? I don't see basic where clause q for json fields on SO.
Something like this, but of course these dont work:
SELECT * from my_table where meta_data->name = 'bob';
SELECT * from my_table where meta_data[name] IS NOT NULL;
Some examples of how to query a json data type field:
SELECT * FROM users WHERE JSON_EXTRACT(meta_data, "$.first_name") = 'bob';
SELECT * FROM users WHERE JSON_EXTRACT(meta_data, "$.age") IS NOT NULL;
SELECT * FROM users WHERE JSON_EXTRACT(meta_data, "$.accepted_policy") = true;
With mysql 5.7.9 +
You can also just do this (shortcut for JSON_EXTRACT):
SELECT * FROM users WHERE meta_data->"$.first_name" = 'bob'
You might notice your json data results are "quoted". You could use JSON_UNQUOTE, or you could use this, which is a shortcut of JSON_EXTRACT & JSON_UNQUOTE:
SELECT meta_data->>"$.first_name" FROM users WHERE meta_data->>"$.first_name" IS NOT NULL
And to select data from within sub objects:
SELECT meta_data->>"$.address.tel" FROM users WHERE meta_data->>"$.address.street" = "123 Main St"
docs: https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html
Use Below query for WHERE operation with JSON Datatype Field in mysql
SELECT meta_data->'$.first_name' meta_data FROM users WHERE INSTR(meta_data->'$.first_name','123') > 0

How to use an excludent IN?

I have the following code:
SELECT stores_tb.stores, sum(products_tb_tb.prices)
from products_tb
inner join stores_tb
on products_tb.id_store = stores_tb.id_store
where products_tb.barcode IN ($barcodes)
group by stores_tb.stores
order by sum(products_tb.prices)
Being the $barcodes an array (already converted to a string) that I receive via ajax in a php file that executes the MySQL.
The thing is that the IN is inclusive, using OR for each of the array values, meaning that if one of the stores required on the SELECT have one, but not all of the barcodes in the array, it will be shown.
I wanna know if there is a function like the IN (or a way to use the IN function) in which it will return only the stores that have all of the barcodes passed in the array, the equvilant of using AND instead of OR for each of the array values.
You can do this with a having clause:
select s.stores, sum(p.prices)
from products_tb p join
stores_tb s
on p.id_store = s.id_store
where p.barcode IN ($barcodes)
group by s.stores
having count(distinct p.barcode) = $n -- the number of codes that need to match
order by sum(p.prices);
The $n value is the length of the $barcodes list (strictly speaking, the number of unique items in it).
Instead of an array and an IN clause You could use a subselect and the ALL operator
SELECT stores_tb.stores, sum(products_tb_tb.prices)
from products_tb
inner join stores_tb
on products_tb.id_store = stores_tb.id_store
where products_tb.barcode = ALL (
select barcode from my_table )
)
group by stores_tb.stores
order by sum(products_tb.prices)
https://dev.mysql.com/doc/refman/5.7/en/any-in-some-subqueries.html
https://dev.mysql.com/doc/refman/5.7/en/all-subqueries.html

Doctrine2 DBAL Exists query

I would like to ask for your help with Doctrine2 DBAL query built with QueryBuilder. I'm used to ORM, but I think it's an overkill for such query which is being called in a listener.
I need a query with SELECT EXISTS and I don't know how I can construct it using DBAL QueryBuilder.
I have a subquery already created:
$subQuery = $connection->createQueryBuilder();
$subQuery
->select('o.id')
->from('order', 'o')
->leftJoin('o', 'payment', 'p')
->where($subQuery->expr()->isNull('p.id'))
;
I basically want to check if there are any unpaid orders. I now have no idea how to build the SELECT EXISTS query? Can anyone point me in the right direction? I was thinking about something like this:
$qb->select('EXISTS(?)')->setParameter($subQuery->getDQL())
Will that be the correct solution?
#EDIT
After a while of thinking I decided to use ORM instead. Unfortunately that did not work either, I'm getting an error:
line 0, col 7: Error: Expected known function, got 'EXISTS'
The DQL is:
SELECT EXISTS(<subquery here>)
It is a bit weird considering that It has been build with QueryBuilder:
/* #var $qb QueryBuilder */
$qb = $this->em->createQueryBuilder();
$qb
->select($qb->expr()->exists($subQuery->getDQL()));
A few years late, but you need to specify your EXISTS subquery SQL within the SELECT or WHERE statement portion of the QueryBuilder, as opposed to using a parameter.
Additionally since order is a reserved word in MySQL, you will need to use identifier quotes ` (back-tick) to escape the table name.
When using the ORM; you must specify a FROM statement that references an entity, so you would need to change your approach.
$connection = $this->em->getConnection();
$expr = $connection->getExpressionBuilder();
$qbSub = $connection->createQueryBuilder()
->select('1')
->from('`order`', 'o')
->leftJoin('o', '`payment`', 'p', $expr->eq('p.order_id', 'o.id'))
->where($expr->isNull('p.id'));
/**
* #return string "1" if a record exists, "0" otherwise
*/
$connection->createQueryBuilder()
->select('EXISTS(' . $qbSub->getSQL() . ')')
->execute()
->fetchColumn();
Resulting SQL
SELECT EXISTS(
SELECT 1
FROM `order` AS o
LEFT JOIN `payment` AS p
ON p.order_id = o.id
WHERE p.id IS NULL
);
Note: If you have any parameters, the values for the placeholders must be bound using QueryBuilder::setParameter() on the top-level
query, not the sub-queries.
$qbSub = $connection->createQueryBuilder()
->select('1')
->from('`order`', 'o')
->leftJoin('o', '`payment`', 'p', $expr->andX(
$expr->eq('p.order_id', 'o.id'),
$expr->eq('p.name', ':name') // subquery placeholder
))
->where($expr->isNull('p.id'));
$connection->createQueryBuilder()
->select('EXISTS(' . $qbSub->getSQL() . ')')
->setParameter('name', $value) // subquery placeholder param value
->execute()
->fetchColumn();
However, I suggest changing your query from an exclusion join to an inclusion join with NOT EXISTS. Doing so will filter orders that have been paid, out of your result-set. Instead of attempting to join every order on every payment and retrieve the payments that return null. Dramatically improving the performance of the query.
Example db-fiddle
SELECT EXISTS (
SELECT 1
FROM `order` AS o
WHERE NOT EXISTS(
SELECT NULL
FROM `payment` AS p
WHERE p.order_id = o.id
)
)

"NOT IN" for Active Record

I have a MySQL query that I am trying to chain a "NOT IN" at the end of it.
Here is what it looks like in ruby using Active Record:
not_in = find_by_sql("SELECT parent_dimension_id FROM relations WHERE relation_type_id = 6;").map(&:parent_dimension_id)
joins('INNER JOIN dimensions ON child_dimension_id = dimensions.id')
.where(relation_type_id: model_relation_id,
parent_dimension_id: sub_type_ids,
child_dimension_id: model_type)
.where.not(parent_dimension_id: not_in)
So the SQL query I'm trying to do looks like this:
INNER JOIN dimensions ON child_dimension_id = dimensions.id
WHERE relations.relation_type_id = 5
AND relations.parent_dimension_id
NOT IN(SELECT parent_dimension_id FROM relations WHERE relation_type_id = 6);
Can someone confirm to me what I should use for that query?
do I chain on where.not ?
If you really do want
SELECT parent_dimension_id
FROM relations
WHERE relation_type_id = 6
as a subquery, you just need to convert that SQL to an ActiveRecord relation:
Relation.select(:parent_dimension_id).where(:relation_type_id => 6)
then use that as a value in a where call the same way you'd use an array:
not_parents = Relation.select(:parent_dimension_id).where(:relation_type_id => 6)
Relation.joins('...')
.where(relation_type_id: model_relation_id, ...)
.where.not(parent_dimension_id: not_parents)
When you use an ActiveRecord relation as a value in a where and that relation selects a single column:
r = M1.select(:one_column).where(...)
M2.where(:column => r)
ActiveRecord is smart enough to inline r's SQL as an in (select one_column ...) rather than doing two queries.
You could probably replace your:
joins('INNER JOIN dimensions ON child_dimension_id = dimensions.id')
with a simpler joins(:some_relation) if your relations are set up too.
You can feed where clauses with values or arrays of values, in which case they will be translated into in (?) clauses.
Thus, the last part of your query could contain a mapping:
.where.not(parent_dimension_id:Relation.where(relation_type_id:6).map(&:parent_dimension_id))
Or you can prepare a statement
.where('parent_dimension_id not in (?)', Relation.where(relation_type_id:6).map(&:parent_dimension_id) )
which is essentially exactly the same thing