Rewrite sql query to Zend_Db_Select - sql

I'm trying to build the following sql query as a Zend_Db_Select object
$sql = "
SELECT
u.id,
u.email,
s.nic as nic,
(SELECT COUNT(*) FROM event WHERE user_id = u.id AND event='login') as logins,
(SELECT COUNT(*) FROM event WHERE user_id = u.id AND event='export') as exports,
(SELECT MAX(time) FROM event WHERE user_id = u.id AND event='login') as lastlogin,
(DATEDIFF(u.expire_date, NOW())) as daysleft
FROM
user u,
seller s
WHERE
u.seller_id = s.id";
with no luck. I can't get the subquerys working in the Zend_Db_Select object.
Is it possible to achieve the same same result by joins instead of subquerys?
Any hints on how to get this working would be highly appreciated.

Try something like this:
$select->from(array('u'=>'user'),array('id','email'));
$select->join(array('s'=>'seller'),'s.id = u.seller_id', array('nic'));
$select->columns(array(
'logins' =>"(SELECT COUNT(*) FROM event WHERE user_id = u.id AND event='login')",
'exports' =>"(SELECT COUNT(*) FROM event WHERE user_id = u.id AND event='export')",
'lastLogin' =>"(SELECT MAX(time) FROM event WHERE user_id = u.id AND event='login')",
'daysLeft' =>"(DATEDIFF(u.expire_date, NOW()))",));
$stmt = $select->query();
var_dump($stmt->fetchAll());

Related

How to use a dynamically created column in a WHERE clause

I created a table column called name cause the firstname and the lastname are stored in two separate columns. To solve the problem I use CONCAT in the SELECT statement. However if I want to search for a specific value inside the name column SQL returns the error:
Unknown column 'name' in 'where clause'
Is it possible to search in a dynamically created column in a SELECT statement?
SELECT
u.ID AS id,
CONCAT(umFirstName.meta_value, " ", umLastName.meta_value) AS name,
u.user_email AS email,
umChatStatus.meta_value AS chatStatus,
umCallStatus.meta_value AS callStatus,
umRef.meta_value AS reference
FROM
wp_users AS u
LEFT JOIN
wp_usermeta AS um ON u.ID = um.user_id
LEFT JOIN
wp_usermeta AS umFirstName ON u.ID = umFirstName.user_id
AND umFirstName.meta_key = "_ctbFirstName"
LEFT JOIN
wp_usermeta AS umLastName ON u.ID = umLastName.user_id
AND umLastName.meta_key = "_ctbLastName"
LEFT JOIN
wp_usermeta AS umChatStatus ON u.ID = umChatStatus.user_id
AND umChatStatus.meta_key = "_ctbChatSessionStatus"
LEFT JOIN
wp_usermeta AS umCallStatus ON u.ID = umCallStatus.user_id
AND umCallStatus.meta_key = "_ctbCallSessionStatus"
LEFT JOIN
wp_usermeta AS umRef ON u.ID = umRef.user_id
AND umRef.meta_key = "_ctbRef"
WHERE
um.meta_key = "wp_capabilities"
AND um.meta_value IN ('a:1:{s:8:"employee ";b:1;}')
AND u.id LIKE '%%'
OR name LIKE '%%'
OR u.user_email LIKE '%%'
OR umChatStatus.meta_value LIKE '%%'
OR umCallStatus.meta_value LIKE '%%'
OR umRef.meta_value LIKE'%%'
ORDER BY
id DESC
LIMIT 0, 20
UPDATE
Got it working like it should. Thanks to Caius Jard metioning to -always use parentheses- in a AND and OR mix. For someone that is interested in the solution.
SQL queries are evaluated in the following order (just the blocks you've used):
FROM (including joins)
WHERE
SELECT
ORDER BY
This means that something you create in the SELECT, like CONCAT(Firstname, ' ', LastName) simply doesn't exist at the time the WHERE is evaluated
You have a couple of options:
Use CONCAT(Firstname, ' ', LastName) in the WHERE clause too
SELECT
CONCAT(Firstname, ' ', LastName) AS N
FROM
person
WHERE
CONCAT(Firstname, ' ', LastName) = 'John smith'
Turn the whole query into a sub query and use the thing you created in the outer
SELECT
x.N
FROM
(
SELECT
CONCAT(Firstname, ' ', LastName) AS N
FROM
person
) x
WHERE
x.N = 'John smith'
This form can also be written as:
WITH x AS
(
SELECT
CONCAT(Firstname, ' ', LastName) AS N
FROM
person
)
SELECT x.N
FROM x
WHERE x.N = 'John smith'
These latter forms are useful when you want to use it multiple times and don't want to keep repeating some massive calculation:
WITH x AS
(
SELECT
SUM(Points) / DATEDIFF(day, MIN(GameStart), MAX(GameEnd)) as PointsPerDay
FROM
Games
GROUP BY PlayerId
)
SELECT
CASE
WHEN PointsPerDay < 100 THEN 'Newbie'
WHEN PointsPerDay < 200 THEN 'Seasoned'
WHEN PointsPerDay < 300 THEN 'Advanced'
ELSE 'Pro'
END as Grading
FROM x
To have to keep repeating a calculation in order to do it all in one query is a bit ugly:
SELECT
CASE
WHEN SUM(Points) / DATEDIFF(day, MIN(GameStart), MAX(GameEnd)) < 100 THEN 'Newbie'
WHEN SUM(Points) / DATEDIFF(day, MIN(GameStart), MAX(GameEnd)) < 200 THEN 'Seasoned'
WHEN SUM(Points) / DATEDIFF(day, MIN(GameStart), MAX(GameEnd)) < 300 THEN 'Advanced'
ELSE 'Pro'
END as Grading
FROM
Games
GROUP BY PlayerId
And indeed because a GROUP BY is evaluated after a WHERE, if you want to use the result of a group by in a where you must do it as a "sub query that groups"/"outer query that wheres" pair
You can directly compare the concatenated value in the where clause like so:
CONCAT(umFirstName.meta_value, " ", umLastName.meta_value) LIKE '%abc%'
SELECT
u.ID AS id,
CONCAT(umFirstName.meta_value, " ", umLastName.meta_value) AS name,
u.user_email AS email,
umChatStatus.meta_value AS chatStatus,
umCallStatus.meta_value AS callStatus,
umRef.meta_value AS reference
FROM wp_users AS u
LEFT JOIN wp_usermeta AS um ON u.ID = um.user_id
LEFT JOIN wp_usermeta AS umFirstName ON u.ID = umFirstName.user_id AND
umFirstName.meta_key = "_ctbFirstName"
LEFT JOIN wp_usermeta AS umLastName ON u.ID = umLastName.user_id AND
umLastName.meta_key = "_ctbLastName"
LEFT JOIN wp_usermeta AS umChatStatus
ON u.ID = umChatStatus.user_id AND umChatStatus.meta_key = "_ctbChatSessionStatus"
LEFT JOIN wp_usermeta AS umCallStatus
ON u.ID = umCallStatus.user_id AND umCallStatus.meta_key = "_ctbCallSessionStatus"
LEFT JOIN wp_usermeta AS umRef ON u.ID = umRef.user_id AND umRef.meta_key = "_ctbRef"
WHERE um.meta_key = "wp_capabilities"
AND um.meta_value IN ('a:1:{s:8:"employee ";b:1;}')
AND u.id LIKE '%%'
OR CONCAT(umFirstName.meta_value, " ", umLastName.meta_value) LIKE '%%'
OR u.user_email LIKE '%%'
OR umChatStatus.meta_value LIKE '%%'
OR umCallStatus.meta_value LIKE '%%'
OR umRef.meta_value LIKE'%%'
ORDER BY id DESC
LIMIT 0, 20

Arel wth lateral table in the from clause,

I have 3 table users, roles and roles_users where in the last table is the intermediate table. So, user can have multiple roles and any role can belong to multiple users.
The problem statement is that we need to render the list of users and their roles matching the search criteria.
select users.id, dynamic_roles.name
from users, lateral (
select GROUP_CONCAT( DISTINCT( roles.name ) ) as name
from roles, roles_users
where (
( roles_users.user_id = users.id AND roles_users.role_id = roles.id )
)
) dynamic_roles
where dynamic_roles.name LIKE '%admin%' AND dynamic_roles.name LIKE '%manager%';
What I tried is as below:
rs = Role.joins(:users).select("GROUP_CONCAT( DISTINCT( #{Role.table_name}.name ) ) as name")
users = User.arel_table #predefined reference received as argument to a method that is supposed to compose the arel query.
users = users.project(users['id']).distinct
users.to_sql
=> "SELECT DISTINCT users.id FROM users"
users.from('dynamic_roles').to_sql
=> "SELECT DISTINCT users.id FROM dynamic_roles"
users.lateral('dynamic_roles').to_sql
=> TypeError: Cannot visit Arel::Nodes::Lateral
from /Users/prasadsurase/.rvm/gems/ruby-2.7.1/gems/activerecord-6.0.3.6/lib/arel/visitors/visitor.rb:39:in `rescue in visit'
Caused by NoMethodError: undefined method `visit_Arel_Nodes_Lateral' for #<Arel::Visitors::MySQL:0x00007f97424477b8>
Referring https://apidock.com/rails/v6.0.0/Arel/SelectManager/from and https://apidock.com/rails/v6.0.0/Arel/SelectManager/lateral
Your issue is that the mysql visitors do not include a visitor for Lateral (the way postgres does).
We could go about adding our own visitor but it is easier to get around this using a NamedFunction instead since the LATERAL syntax is the same as a function.
As with most things complicated, this is not the prettiest solution but it will provide the desired result.
roles = Arel::Table.new('roles')
role_users = Arel::Table.new('role_users')
users = Arel::Table.new('users')
group = Arel::Nodes::NamedFunction.new('GROUP_CONCAT',
[Arel::Nodes::Grouping.new(roles[:name])],
'name')
group.distinct = true
sub = roles.project(group)
.from([roles, role_users])
.where( role_users[:user_id].eq(users[:id]).and(
role_users[:role_id].eq( roles[:id]))
)
dynamic_roles = Arel::Table.new('dynamic_roles')
lateral = Arel::Nodes::NamedFunction.new('LATERAL',[sub], dynamic_roles.name)
users.project(users[:id], dynamic_roles[:name])
.from([ users, lateral])
.where( dynamic_roles[:name].matches(Arel::Nodes::Quoted.new("%admin%")).and(
dynamic_roles[:name].matches(Arel::Nodes::Quoted.new("%manager%"))
))
This will Produce the following SQL:
SELECT users.id, dynamic_roles.name
FROM users, LATERAL(
(
SELECT GROUP_CONCAT( DISTINCT (roles.name)) AS name
FROM roles, role_users
WHERE role_users.user_id = users.id AND role_users.role_id = roles.id
)
) AS dynamic_roles
WHERE
dynamic_roles.name LIKE '%admin%' AND dynamic_roles.name LIKE '%manager%'

How to get categories and sub categories sql

I am trying to get all categories with all sub categories using this SQL query:
SELECT
sections.Section_Name,
sections.Section_Page,
topics.Topic_Name,
topics.Topic_Descr,
topics.Section_ID,
(select count(*) from threads WHERE threads.Topic_ID= topics.Topic_ID and threads.Topic_ID=topics.Topic_ID) as Thread,
(select count(*) from threads join posts on posts.Thread_ID= threads.Thread_ID where threads.Topic_ID =topics.Topic_ID) as Post,
**(SELECT Thread_Time
FROM
(SELECT Thread_Time FROM threads
WHERE threads.Topic_ID = Topic_ID
ORDER BY threads.Thread_ID Desc limit 1) AS Time) AS Time**
FROM
topics, sections
WHERE
topics.Section_ID = sections.Section_ID
It works fine but the Thread_Time query shows the same date on all rows (records)?
Using subqueries is fine for this query. If you want the most recent time, then I think you can do:
select s.Section_Name, s.Section_Page,
t.Topic_Name, t.Topic_Descr, t.Section_ID,
(select count(*)
from threads th
where th.Topic_ID = t.Topic_ID
) as Thread,
(select count(*)
from threads th join
posts p
on p.Thread_ID = th.Thread_ID
where th.Topic_ID = t.Topic_ID
) as Post,
(select max(th.Thread_Time)
from threads th
where th.Topic_ID = t.Topic_ID
)as Time
from topics t join
sections s
on t.Section_ID = s.Section_ID;
The problem with your query is this clause: threads.Topic_ID=Topic_ID. This is just referencing the value in the subquery.

T-SQL Run multiple queries against a subquery

I have a simple data model for users and their requests:
User
- id,
- name
Request
- id,
- createdAt,
- completedAt,
- status
- userId (FK to user)
I'm trying to run a query which collects some stats for every user. The issue is that I have to run the same subquery to fetch user requests for every parameter I select. As instead, I want to run it once and then calculate some stats over it.
select
u.id as UserId,
(select count(*)
from Requests r
where userId = u.id
and timestamp > #dateFrom) as Total,
(select count(*)
from Requests r
where userId = u.id
and timestamp > #dateFrom
and status = N'Completed') as Completed,
(select status
from Requests r
where userId = u.id
and timestamp > #dateFrom
and status != N'Completed') as ActiveStatus,
(select datediff(second, createdAt, completedAt)
from Requests r
where userId = u.id
and timestamp > #dateFrom
and status == N'Completed') as AvgProcessingTime
from User u
Obviously, this query is very slow and I need to optimize it. I tried join, apply, rank, nothing worked out well for me (read as I wasn't able to complete the query for all required stats).
What is the best approach here from the performance stand point?
try this using Left Join and aggregation
There could be a couple of issues here but let me know how you go.
select
u.id as UserId
,count(r.UserId) [Total]
,sum(iif(r.status = N'Completed',1,0)) [Completed]
,sum(iif(r.status <> N'Completed',1,0)) [ActiveStatus]
,avg(iif(r.status = N'Completed', datediff(second, createdAt, completedAt),0)) [AvgProcessingTime]
from User u
left join Request R
where timestamp > #datefrom
and r.userId = u.id
group by
u.id
I'm not sure about this query cause I haven't run it on my machine, but you can give it a try and make some changes accordingly if needed --
SELECT U.ID AS USERID
,COUNT(R.ID) AS TOTAL
,SUM(CASE
WHEN R.[STATUS] = N'COMPLETED'
THEN 1
END) AS [COMPLETED]
,CASE
WHEN R.[STATUS] <> N'COMPLETED'
THEN [STATUS]
END AS [ACTIVE STATUS]
,CASE
WHEN R.[STATUS] = N'COMPLETED'
THEN DATEDIFF(SECOND, CREATEDAT, COMPLETEDAT)
END AS [AVG PROCESSING TIME]
FROM USERS U
LEFT JOIN REQUESTS R ON U.ID = R.USERID
WHERE TIMESTAMP > #DATEFROM

Why is the same SQL sometimes report errors, some time it runs fine?

This is the SQL statement:
select
ua.*, uuu.user_name
from
(select
ud.dev_id,
(select uud.user_id as user_id
from user_device uud
where ud.dev_id = uud.dev_id and assigned = 1) user_id,
(select count(1)
from user_device du
where du.dev_id = ud.dev_id) user_number,
de.license
from
user_device ud
inner join
device de on ud.dev_id = de.dev_id
where ud.user_id = 'XXXXXX') ua
left join
user_info uuu on uuu.user_id = ua.user_id
Execute the same SQL, it sometimes reports this error, but sometimes it runs just fine.
The error :
and this is what I want (with another user_id yesterday)
The error is pretty self-explanatory. I'm pretty sure it is referring to this subquery:
(select uud.user_id
from user_device uud
where ud.dev_id = uud.dev_id and assigned = 1
)
Clearly, this subquery is returning multiple rows under some circumstances. A quick and dirty fix is to add and rownum = 1 to the where clause.
You can determine where the duplicates are by running:
select uud.dev_id, count(*) as cnt
from user_device uud
where uud.assigned = 1
group by uud.dev_id
having count(*) > 1;