Double Left Joins Ignoring Filter - postgresql

I have a table with visits, another with customers and another with locations (where each one belongs to).
I want to display the count on visits on each location. The desired results would be:
visit_count | location_name
-----------------------------
27 | location_1
1 | location_2
0 | location_3
This is my query:
SELECT COUNT(visits.visit_date) as visit_count, locations.location_name FROM locations
LEFT JOIN VISITS ON locations.location_name = visits.location_checkin
LEFT JOIN customer ON visits.cust_id = customer.cust_id
WHERE locations.group_id = 1 AND customer.adm = false AND customer.super_adm = false
GROUP BY locations.id
But the result only gives me:
visit_count | location_name
-----------------------------
27 | location_1
1 | location_2
It's the correct data, but erases the location with zero visits. I tried putting the
customer.adm = false and customer.super_adm = false on the left join on customer, but that list all three locations but ignores the filtering of the false statements.

You need to move condition to ON clasue:
SELECT COUNT(customer.cust_id) as visit_count, locations.location_name
FROM locations
LEFT JOIN VISITS ON locations.location_name = visits.location_checkin
LEFT JOIN customer ON visits.cust_id = customer.cust_id
AND customer.adm = false
AND customer.super_adm = false
WHERE locations.group_id = 1
GROUP BY locations.id, locations.location_name -- here added location_name to match SELECT
If you use WHERE on outer table column reference it will work as normal INNER JOIN

Using left join you should not use left table columns in where .. (this work as an inner join )
instead add these condition to the related on clause
SELECT COUNT(visits.visit_date) as visit_count, locations.location_name
FROM locations
LEFT JOIN VISITS ON locations.location_name = visits.location_checkin
LEFT JOIN customer ON visits.cust_id = customer.cust_id
AND customer.adm = false AND customer.super_adm = false
WHERE locations.group_id = 1
GROUP BY locations.id

Related

Recursive CTE ancestry (Employee with multiple Managers)

I am having difficulties trying to output all managers above an employee. The employees table does not have a manager_id column, which makes this a bit complicated.
The hierarchy is defined in a references table.
Each employee belongs to a department and is assigned a position. A position with level 1 is a manager, level 0 is non-manager.
To get the manager of a manager, we take the parent_id of the current department and look for an employee assigned with a position with level 1.
The function I currently have only returns the employee's direct manager (from the non-recursive term).
CREATE OR REPLACE FUNCTION public.get_employee_managers(employees_row employees)
RETURNS SETOF employees
LANGUAGE sql
STABLE
AS $function$
WITH RECURSIVE managers AS (
SELECT e1.*
FROM
employees e1
JOIN "references" AS employees_row_department ON employees_row_department.id = employees_row.department_id
JOIN "references" AS e1_department ON e1_department.id = e1.department_id
JOIN "references" AS e1_position ON e1_position.id = e1.position_id
WHERE
e1_department.id = employees_row_department.parent_id AND e1_position.level = 1 AND e1.active = 1 AND e1.is_deleted = false
OR
e1_department.id = employees_row.department_id AND e1_position.level = 1 AND e1.active = 1 AND e1.is_deleted = false AND e1.id <> employees_row.id
UNION
SELECT m1.*
FROM
managers m1
JOIN "references" AS m1_department ON m1_department.id = m1.department_id
JOIN "references" AS m1_position ON m1_position.id = m1.position_id
INNER JOIN employees AS e2 ON (m1_department.parent_id = e2.department_id AND m1_position.level = 1 AND e2.active = 1 AND e2.is_deleted = false)
)
SELECT * FROM managers ORDER BY department_id ASC
$function$
Using the following recursive term gives me the same result as the one above
SELECT e2.*
FROM
employees e2
JOIN "references" AS e2_department ON e2_department.id = e2.department_id
JOIN "references" AS e2_position ON e2_position.id = e2.position_id
INNER JOIN managers m1 ON m1.id = e2.id
JOIN "references" AS m1_department ON m1_department.id = m1.department_id
WHERE
e2_department.id = m1_department.parent_id AND e2_position.level = 1 AND e2.active = 1 AND e2.is_deleted = false
Changing the second recursive term seems to give me the result I was looking for.
INNER JOIN managers m1 ON m1.id = e2.id
to
INNER JOIN managers m1 ON m1.id <> e2.id

Jsonb_object_keys() does not return any rows in left join if the right side table does not have any matching records

This is db query .
select users.Id,jsonb_object_keys(orders.metadata::jsonb) from users left join orders on users.userId=orders.userId where users.userId=2;
users table orders table
------------------- -----------------------------------------------------
|userId| name | | userId|orderId|metadata |
| 1 | john | | 1 | 1 | {"orderName":"chess","quantity":1}|
| 2 | doe | | 1 | 2 | {"orderName":"cube" ,"quantity":1}|
------------------- -----------------------------------------------------
Why there are no rows returned by the query ?
Very Nice and tricky question. to achieve what you want you should try below query:
select
t1.userid,
t2.keys
from
users t1
left join (select userid, orderid, jsonb_object_keys(metadata) as keys from orders) t2
on t1.userid=t2.userid
Your Query seems correct but there is catch. When you are left joining both tables without jsonb_object_keys(metadata), it will work as you are expecting. But when you use with this function then this function will return a set of records for each rows of select statement and perform simple join with rest of the columns internally. That's why it will remove the rows having NULL value in second column.
You should left join to the result of the jsonb_each() call:
select users.userid, meta.*
from users
left join orders on users.userid = orders.userid
left join jsonb_object_keys(orders.metadata::jsonb) as meta on true
where users.userid = 2;

How to use 'Distinct' for just one column?

I have a query checking the visits from some "locations" table I have. If the user signed up with a referral of "emp" or "oth", their first visit shouldn't count but the second visit and forward should count.
I'm trying to get a count of those "first visits" per location. Whenever they do a visit, I get a record on which location it was.
The problem is that my query is counting correctly, but some users have visits on different locations. So instead of just counting one visit for that location (the first one), is adding one per location where a user has done a visit.
This is my query
SELECT COUNT(DISTINCT CASE WHEN customer.ref IN ('emp', 'oth') THEN customer.id END) as visit_count, locations.name as location FROM locations
LEFT JOIN visits ON locations.location_name = visits.location_visit_name
LEFT JOIN customer ON customer.id = visits.customer_id
WHERE locations.active = true
GROUP BY locations.location_name, locations.id;
The results I'm getting are
visit_count | locations
-------------------------
7 | Loc 1
3 | Loc 2
1 | Loc 3
How it should be:
visit_count | locations
-------------------------
6 | Loc 1
2 | Loc 2
1 | Loc 3
Because 2 of these people have visits on both locations, so its counting one for each location. I think the DISTINCT is also doing it for the locations, when it should be only on the counting for the customer.id
Is there a way I can add something to my query to just grab the location for the first visit, without caring they have done other visits on other locations?
If I followed you correctly, you want to count only the first visit of each customer, spread by location.
One solution would be to use a correlated subquery in the on clause of the relevant join to filter on first customer visits. Assuming that column visit(visit_date) stores the date of each visit, you could do:
select
count(c.customer_id) visit_count,
l.name as location
from locations l
left join visits v
on l.location_name = v.location_visit_name
and v.visit_date = (
select min(v1.visit_date)
from visit v1
where v1.customer_id = v.customer_id
)
left join customer c
on c.id = v.customer_id
and c.ref in ('emp', 'oth')
where l.active = true
group by l.location_name, l.id;
Side notes:
properly fitering on the first visit per customer avoids the need for distinct in the count() aggregate function
table aliases make the query more concise and easier to understand; I recommend to use them in all queries
the filter on customer(ref) is better placed in the where clause than as a conditional count criteria
Try moving the when condition in where clause
SELECT COUNT( distinct customer.id) as visit_count
, locations.name as location
FROM locations
LEFT JOIN visits ON locations.location_name = visits.location_visit_name
LEFT JOIN customer ON customer.id = visits.customer_id
WHERE locations.active = true
AND customer.ref IN ('emp', 'oth')
GROUP BY locations.location_name;c

Avoid duplication in SQL Server

I got the below result when i run this query.
SELECT DISTINCT PT.F_PRO AS F_PRODUCT, PT.F_TEXT_CODE AS F_TEXT_CODE, PHT.F_PHRASE AS F_PHRASE FROM T_PROD_TEXT PT
LEFT JOIN T_P_LINKAGE PHL
ON PT.F_TEXT_CODE = PHL.F_TEXT_CODE
INNER JOIN T_P_TRANSLATIONS PHT
ON PHL.F_PHRASE_ID = PHT.F_PHRASE_ID
WHERE PT.F_DATA_CODE = 'MANU' AND PHT.F_LANGUAGE = 'EN'
OUTPUT
F_PRODUCT F_TEXT_CODE F_PHRASE
294264_B MANU0008 Alcoa, Inc
294264_B MANU0012 BioSensory
00091A MANU0006 3M Company
00094A MANU0006 4M Company
00094A MANU0006 5M Company
The above query returns duplication in F_PRODUCT COLUMN.i want to display F_product without duplication. only one record should display for each F_product.(First record) without using top command
Required Output
F_PRODUCT F_TEXT_CODE F_PHRASE
294264_B MANU0008 Alcoa, Inc.
00091A MANU0006 3M Company|par
You can use row_number() to assign a number to each row within a group of f_pro. Then retrieve only rows that are number 1. You can change the order by if something else determines the order.
SELECT *
FROM
(SELECT PT.F_PRO AS F_PRODUCT, PT.F_TEXT_CODE AS F_TEXT_CODE, PHT.F_PHRASE AS F_PHRASE, ROW_NUMBER() OVER (PARTITION BY PT.F_PRO ORDER BY PHT.F_PHRASE ASC) AS RowNum
FROM T_PROD_TEXT PT
LEFT JOIN T_P_LINKAGE PHL
ON PT.F_TEXT_CODE = PHL.F_TEXT_CODE
INNER JOIN T_P_TRANSLATIONS PHT
ON PHL.F_PHRASE_ID = PHT.F_PHRASE_ID
WHERE PT.F_DATA_CODE = 'MANU' AND PHT.F_LANGUAGE = 'EN') dt
WHERE RowNum = 1
SELECT PT.F_PRO AS F_PRODUCT,
MIN(PT.F_TEXT_CODE) AS F_TEXT_CODE,
MIN(PHT.F_PHRASE) AS F_PHRASE FROM T_PROD_TEXT PT
LEFT JOIN T_P_LINKAGE PHL
ON PT.F_TEXT_CODE = PHL.F_TEXT_CODE
INNER JOIN T_P_TRANSLATIONS PHT
ON PHL.F_PHRASE_ID = PHT.F_PHRASE_ID
WHERE PT.F_DATA_CODE = 'MANU' AND PHT.F_LANGUAGE = 'EN'
group By PT.F_PRO;
is one way to do that. It doesn't do it for the "FIRST" since it is vague how would you define the "FIRST".

JOIN inside a SELECT MAX in Postgres

I have the following query that returns these two elements.
SELECT ticket_holds.id, ticket_holds.created_at, orders.user_id, users.id, charges.payment_method
FROM ticket_holds
LEFT JOIN order_items ON order_items.id = ticket_holds.order_item_id
LEFT JOIN orders ON orders.id = order_items.order_id
LEFT JOIN users ON users.id = orders.user_id
LEFT JOIN charges on charges.order_id = orders.id;
This result is okay.
id | created_at | user_id | id | payment_method
----+----------------------------+---------+----+----------------
1 | 2017-05-26 04:29:22.189628 | 4 | 4 |
2 | 2017-05-26 04:33:00.721404 | 4 | 4 | some_payment_method
When I try to filter the query to display the last record created per user it also works okay using SELECT MAX.
SELECT ticket_holds.id, ticket_holds.created_at, orders.user_id, users.id, charges.payment_method
FROM ticket_holds
LEFT JOIN order_items ON order_items.id = ticket_holds.order_item_id
LEFT JOIN orders ON orders.id = order_items.order_id
LEFT JOIN users ON users.id = orders.user_id
LEFT JOIN charges on charges.order_id = orders.id
WHERE ticket_holds.created_at = (SELECT MAX(ticket_holds.created_at) FROM ticket_holds WHERE orders.user_id = users.id);
And the result
id | created_at | user_id | id | payment_method
----+----------------------------+---------+----+----------------
2 | 2017-05-26 04:33:00.721404 | 4 | 4 | some_payment_method
(1 row)
However, when Im trying to filter to only select items without a payment_method I get no results.
SELECT ticket_holds.id, ticket_holds.created_at, orders.user_id, users.id, charges.payment_method
FROM ticket_holds
LEFT JOIN order_items ON order_items.id = ticket_holds.order_item_id
LEFT JOIN orders ON orders.id = order_items.order_id
LEFT JOIN users ON users.id = orders.user_id
LEFT JOIN charges on charges.order_id = orders.id
WHERE ticket_holds.created_at = (SELECT MAX(ticket_holds.created_at) FROM ticket_holds WHERE orders.user_id = users.id AND charges.payment_method IS NULL);
(Notice the AND charges.payment_method IS NULL) and the end of the query)
This is what I get instead
id | created_at | user_id | id | payment_method
----+------------+---------+----+----------------
(0 rows)
Any idea how can I retrieve the elements with no payment method, in this case would be the record with id 1?
As per discussion on chat. i think below query will help you to get required result.
with data as ( SELECT ticket_holds.id, ticket_holds.created_at, orders.user_id, users.id, charges.payment_method
FROM ticket_holds
LEFT JOIN order_items ON order_items.id = ticket_holds.order_item_id
LEFT JOIN orders ON orders.id = order_items.order_id
LEFT JOIN users ON users.id = orders.user_id
LEFT JOIN charges on charges.order_id = orders.id
where charges.payment_method is null
)
select * from data d1
where d1.created_at = (select max(created_at) from data d2
where d1.users_id = d2.user_id
)
There are few points while writing an sub query in where clause. Join your table carefully as in your case you are not utilizing any column for joining.