Mysql joins headache - mysql

I have this database. It does not contain all the tables, but for my problem what is included in the picture is enough.
The database is designed for transportation companies.
So, I have a booking. Any booking can have multiple addresses. These types are (routing_type: pick-up, stop, drop-off). My addresses are either a street address or an airport.
Inside routing table is use either the address_id or the airport_id to tell which type the routing is. If airport_id has a number than address_id is NULL.
In my booking_routing table I have as many entries as there are routing types for a specific booking. If I have 3 addresses for a specific booking, I will have 3 entries in booking_routing (maybe Pick-up, then Stop and then Drop-off). Usually there are only two addresses necessary, pick-up an drop-off.
PROBLEM: I try to create a query in which I will have displayed my Pick-up and Drop-off point on the same line with other fields from the booking.
Example:
Booking_ID Account_ID ... Pick-Up Drop-Off
121 32 1 Main St IAD, AA-322
Right now I only managed to get them using LEFT JOIN on two lines.
Like this:
Booking_ID Account_ID ... Address_Type Address
121 32 Pick_up 1 Main St
121 32 Drop-Off IAD, AA-322
SELECT
b.booking_id as ID, a.account_id as Acc_ID,
MAX(CASE WHEN br.routing_type_id = 1 THEN -- 1=pick-up
Concat(ad.address_line1, " ", api.code, " ", aii.code, "-", ap.flight_number) END) as Pick_up,
MAX(CASE WHEN br.routing_type_id = 4 THEN -- 4=drop-off
Concat(ad.address_line1, " ", api.code, " ", aii.code, "-", ap.flight_number) END) as Drop_off
FROM booking b
left JOIN account a ON b.account_id = a.account_id
left join booking_routing br ON b.booking_id = br.booking_id
left join routing r ON br.routing_id = r.routing_id
left join routing_type rt ON br.routing_type_id = rt.routing_type_id
left join address ad ON r.address_id = ad.address_id
left join airport ap ON r.airport_id = ap.airport_id
left join airport_info api ON ap.airport_info_id = api.airport_info_id
left join airline_info aii ON ap.airline_info_id = aii.airline_info_id
group by b.booking_id, a.account_id;

You can achieve this by using MAX and CASE:
SELECT Booking_Id,
Account_Id,
MAX(CASE WHEN Address_Type = 'Pick_up' THEN Address END) Pick_Up,
MAX(CASE WHEN Address_Type = 'Drop-Off' THEN Address END) Drop_Off
FROM YourTables...
GROUP BY Booking_Id, Account_Id
SQL Fiddle Demo

Related

Single query with multiple independent left joins

I have two tables. One for elements (leads) and one for that elements properties (leads_properties).
Leads table columns, useful to solve this issue, are: id, token.
Properties table is something like this:
id label value
1 phone 555333666
3 phone 111222555
3 city Milan
I am trying to extract with a single query a lead and all the requested properties. If i ask for city, i will retrieve all leads info joined wit city property only, if i ask for city and phone i will retrieve even phone.
The query i am trying is:
SELECT leads.* ,
p1.value as 'phone' ,
p2.value as 'city'
FROM leads
LEFT JOIN leads_properties as p1 ON leads.id = p1.lead_id
LEFT JOIN leads_properties as p2 ON leads.id = p2.lead_id
WHERE leads.id = 1
AND p1.label = 'phone'
AND p2.label = 'city'
AND leads.token = 'xxxxx'
LIMIT 1
This is working great if all the requested proerties are present in DB (in this example for the lead id=3) but fails and return 0 lines for lead id=1 that didn't had "city" property.
I understand why (the AND clause in WHERE statement probably is restrictive) but i don't understand how to solve the issue.
I would like to retrieve a single line like this (if all the requested properties are in DB)
id token city phone
3 xxxxx Milan 111222555
Or like this (with empty or NULL value) if some of the requested properties are not in DB:
id token city phone
1 xxxxx 111222555
Thanks so much.
You can use conditional aggregation using case when expression
SELECT leads.id,max(case when p1.label = 'city' then p1.value end) as city
max(case when p1.label = 'phone' then p1.value end) as phone
FROM leads
LEFT JOIN leads_properties as p1 ON leads.id = p1.lead_id
WHERE leads.id = 1
group by leads.id
It is not AND being restrictive, but rather asking p2.label = 'city'. If you want any property you probably can just ask to avoid duplication. For example:
SELECT leads.* ,
p1.value as 'phone' ,
p2.value as 'city'
FROM leads
LEFT JOIN leads_properties as p1 ON leads.id = p1.lead_id
LEFT JOIN leads_properties as p2 ON leads.id = p2.lead_id
WHERE leads.id = 1
AND p1.label > p2.label
AND leads.token = 'xxxxx'
LIMIT 1

Multiple Filter inside SELECT SQL Statement

I'm writing a SQL Statement to get some values in a Recordset, which I'll use to transfer the result into TextBoxes on a Form in Excel. The tables involved are:
Customers -> CustomerId, FirstName, LastName, TelNumber
Invoice -> InvoiceId, CustomerId, CarModel, CarColor, CarPlate
Repairs -> RepairId, InvoiceId, TypeOfRepair, PartOfCar, Price
Services -> ServiceId, InvoiceId, Date, Status
When a Customer comes to the Garage, an Invoice is created, which is associated with this customer. An invoice can have many Repairs. The customer goes away without repairing the car, but the invoice is there. If the customer decides to repair the car, then a Service is created, which starts with the Status "working on it...". When the service is done, the status change to "Waiting for Check Out..."
I want to use a SQL Statement to retrieve the following Values (columns) for a specific InvoiceId:
CarModel, Color, Plate, CustomerName (FirstName LastName), PaintingTotalValue (where 'Painting' is one type in the column 'Type'), OtherTotalValue (total price of all the other types of repairs in this invoice), total price (total price, that is, painting + other ones).
I wrote the following, to get the values, but I don't know how to get the PaintingTotalValue and OtherTotalVAlue.
SELECT i.CarModel, i.Color, i.Plate, CONCAT(c.FirstName,' ',c.LastName) AS Name, FORMAT(SUM(r.Price),2) AS TotalPrice
FROM Services AS s INNER JOIN Invoices AS i ON s.invoiceId=i.invoiceId
INNER JOIN Repairs AS r ON s.invoiceId=r.invoiceId
INNER JOIN Customers AS c ON i.customerId=c.customerId
WHERE s.invoiceId = 15
Use CASE WHEN in your SELECT clause, to select the value that's conditional to the type:
SELECT
...
CASE WHEN r.Type = 'Painting' THEN r.Price ELSE 0 END PaintWorkPrice,
CASE WHEN r.Type <> 'Painting' THEN r.Price ELSE 0 END OtherWorkPrice,
FROM ...
That's one thing.
The other thing is that you're not selecting anything from the Services table, and making your query much more complicated than it needs to be.
If you can modify the schema, remove the ServiceId primary key field, and use Services.InvoiceId as a primary key instead: that will enforce the 1:1 relationship naturally.
FROM Repairs r
INNER JOIN Invoices i ON r.InvoiceId = i.InvoiceId
INNER JOIN Customers c ON i.CustomerId = c.CustomerId
The data you want to aggregate is granular to Repairs, so you select FROM that, and then move your way through the foreign keys up to Customers.
SELECT
i.CarModel
,i.Color
,i.Plate
,CONCAT(c.FirstName,' ',c.LastName) Name
,CASE WHEN r.Type = 'Painting' THEN r.Price ELSE 0 END PaintWorkPrice
,CASE WHEN r.Type <> 'Painting' THEN r.Price ELSE 0 END OtherWorkPrice
,r.Price
FROM Repairs r
INNER JOIN Invoices i ON r.InvoiceId = i.InvoiceId
INNER JOIN Customers c ON i.CustomerId = c.CustomerId
That's not aggregated yet: there's a record for each repair, for every invoice, under every customer that has an invoice. That part is the sub-query. If you have a parameter, that's where you use it.
WHERE i.InvoiceId = pInvoiceId
If you're just hard-coding an ID, that's where you do it too.
Now type SELECT q.* FROM ( on the line above, and ) q under the WHERE clause, then replace the q.* with the fields you're not aggregating - and aggregate the others. The result should be something like this:
SELECT
q.CarModel
,q.Color
,q.Plate
,q.Name
,SUM(q.PaintWorkPrice) PaintAmount
,SUM(q.OtherWorkPrice) OtherAmount
,SUM(q.Price) TotalAmount
FROM (
SELECT
i.CarModel
,i.Color
,i.Plate
,CONCAT(c.FirstName,' ',c.LastName) Name
,CASE WHEN r.Type = 'Painting' THEN r.Price ELSE 0 END PaintWorkPrice
,CASE WHEN r.Type <> 'Painting' THEN r.Price ELSE 0 END OtherWorkPrice
,r.Price
FROM Repairs r
INNER JOIN Invoices i ON r.InvoiceId = i.InvoiceId
INNER JOIN Customers c ON i.CustomerId = c.CustomerId
WHERE i.InvoiceId = 15
) q
GROUP BY
q.CarModel
,q.Color
,q.Plate
,q.Name

MySQL query to convert normalized data into single row

I'm trying to create a query to format content from normalized tables into a single row.
What I would like is something like this with a single row for each contact:
Name Mobile Office Home
--------------- ----------- ---------- ----------
Fred Flintstone 123-456-7890 234-567-8901 789-012-3456
Barney Rubble 456-789-0123 678-901-2345
Wilma Flintstone 567-890-1234 789-012-3456
What I am getting from my latest query is this, with multiple rows per contact:
Name Phone Phone_Type
--------------- ------------ -----------
Fred Flintstone 123-456-7890 Mobile
Fred Flintstone 234-567-8901 Office
Fred Flintstone 789-012-3456 Home
Barney Rubble 456-789-0123 Mobile
Barney Rubble 678-901-2345 Home
Wilma Flintstone 567-890-1234 Mobile
Wilma Flintstone 789-012-3456 Home
Here are the tables involved (simplified):
contacts
----------
contact_id
name
link_contact_phonenumbers
-------------------------
contact_id
phone_number_id
phone_numbers
-------------
phone_number_id
phone_number
type_id
ref_phone_types
---------------
type_id
name
Here is what I have so far:
SELECT
cn.name as Name,
concat( left(ph.phone_number,3) , "-" , mid(ph.phone_number,4,3) , "-", right(ph.phone_number,4)) as Phone,
pt.name as Phone_Type
FROM
contacts cn
LEFT JOIN link_contact_phonenumbers lp ON lp.contact_id = cn.contact_id
LEFT JOIN phone_numbers ph ON ph.phone_number_id = lp.phone_number_id
LEFT JOIN ref_phone_types pt ON pt.type_id = ph.type_id
I looked at using GROUP_CONCAT() function, but that pulls all the content into a single column. I need them to go into their own columns.
I've been looking into subqueries combined with IF() but haven't figured it out yet.
Is there a way to do this in pure SQL?
It sounds like you can accomplish what you are looking for with a few joins:
Select a.name, c.phone_number, d.phone_number, e.phone_number from contacts a
left join link_contact_phonenumbers b on a.contact_id = b.contact_id
left join phone_numbers c on b.phone_number_id = c.phone_number_id and c.type_id = "whatever id is mobile"
left join phone_numbers d on b.phone_number_id = d.phone_number_id and d.type_id = "whatever id is office"
left join phone_numbers e on b.phone_number_id = e.phone_number_id and e.type_id = "whatever id is home"
I do not know if this is the most efficient way, I also don't have a database to test with right now so it might be off, but it should point you in the right direction. Also might have to group by a.name in case the first join adds multiple rows.
Here is one solution for others that will find this in the future:
SELECT
cn.name as Name,
MAX(CASE WHEN pt.type_id = '1' THEN ph.phone_number ELSE NULL END) as Mobile,
MAX(CASE WHEN pt.type_id = '2' THEN ph.phone_number ELSE NULL END) as Office,
MAX(CASE WHEN pt.type_id = '3' THEN ph.phone_number ELSE NULL END) as Home
FROM
contacts cn
LEFT JOIN link_contact_phonenumbers lp ON lp.contact_id = cn.contact_id
LEFT JOIN phone_numbers ph ON ph.phone_number_id = lp.phone_number_id
LEFT JOIN ref_phone_types pt ON pt.type_id = ph.type_id
GROUP BY cn.contact_id
Found this solution here: How to Denormalize a Normalized Table Using SQL
I'm not sure if this is the most efficient, but it works.

What is wrong with this MySQL query (formatting left join)?

I have a query as follows:
SELECT
staff_names.staff_ID AS sid
staff_names.name AS name,
staff_names.rec_type AS rec_type,
prod_staff.specialized AS specialized,
compspec.name AS compspec_name
FROM staff_names JOIN prod_staff USING (staff_ID)
LEFT JOIN (prod_staff_compspec JOIN company_list USING (comp_ID)) compspec
USING (prod_ID, staff_ID, role_ID)
WHERE prod_staff.role_ID = 2
AND prod_staff.prod_ID = 27
AND prod_staff.asst = 'n'
AND episode IS NOT NULL
ORDER BY name
Running this as-is says there's an error near the 'compspec' alias. Removing that and changing 'compspec' to 'company_list' in the SELECT clause returns no rows, even though it should return 1 with the given values. The left join seems to be the problem, but I don't how it should be formatted.
The prod_staff table has prod_ID, staff_ID and role_ID fields. prod_staff_compspec has these and a comp_ID field. prod_staff may or may not have a matching prod_staff_compspec row, but prod_staff_compspec always has a matching company_list row.
What I want to do is retrieve a list of all staff names associated with a given role_ID and prod_ID in the prod_staff table, as well as a company name from the company_list table, if a link to such exists in the prod_staff_compspec table (only a small minority have one).
Switched to ON to define the table relations. LEFT JOIN (prod_staff_compspec JOIN company_list USING (comp_ID)) compspec is switched to 2 left join.
select a.staff_id sid, a.name, a.rec_type, b.specialized, d.name compspec_name
from staff_names a
join prod_staff b on a.staff_id = b.staff_id
left join prod_staff_compspec c on b.prod_id = c.prod_id and b.staff_id = c.staff_id and b.role_id = c.role_id
left join company_list d on c.comp_id = d.comp_id
where b.role_id = 2 and b.prod_id = 27 and b.asst = 'n' and episode is not null
order by a.name;

SQL: Get latest entries from history table

I have 3 tables
person (id, name)
area (id, number)
history (id, person_id, area_id, type, datetime)
In this tables I store the info which person had which area at a specific time. It is like a salesman travels in an area for a while and then he gets another area. He can also have multiple areas at a time.
history type = 'I' for CheckIn or 'O' for Checkout.
Example:
id person_id area_id type datetime
1 2 5 'O' '2011-12-01'
2 2 5 'I' '2011-12-31'
A person started traveling in area 5 at 2011-12-01 and gave it back on 2011-12-31.
Now I want to have a list of all the areas all persons have right now.
person1.name, area1.number, area2.number, area6.name
person2.name, area5.number, area9.number
....
The output could be like this too (it doesn't matter):
person1.name, area1.number
person1.name, area2.number
person1.name, area6.number
person2.name, area5.number
....
How can I do that?
This question is, indeed, quite tricky. You need a list of the entries in history where, for a given user and area, there is an 'O' record with no subsequent 'I' record. Working with just the history table, that translates to:
SELECT ho.person_id, ho.area_id, ho.type, MAX(ho.datetime)
FROM History AS ho
WHERE ho.type = 'O'
AND NOT EXISTS(SELECT *
FROM History AS hi
WHERE hi.person_id = ho.person_id
AND hi.area_id = ho.area_id
AND hi.type = 'I'
AND hi.datetime > ho.datetime
)
GROUP BY ho.person_id, ho.area_id, ho.type;
Then, since you're really only after the person's name and the area's number (though why the area number can't be the same as its ID I am not sure), you need to adapt slightly, joining with the extra two tables:
SELECT p.name, a.number
FROM History AS ho
JOIN Person AS p ON ho.person_id = p.id
JOIN Area AS a ON ho.area_id = a.id
WHERE ho.type = 'O'
AND NOT EXISTS(SELECT *
FROM History AS hi
WHERE hi.person_id = ho.person_id
AND hi.area_id = ho.area_id
AND hi.type = 'I'
AND hi.datetime > ho.datetime
);
The NOT EXISTS clause is a correlated sub-query; that tends to be inefficient. You might be able to recast it as a LEFT OUTER JOIN with appropriate join and filter conditions:
SELECT p.name, a.number
FROM History AS ho
JOIN Person AS p ON ho.person_id = p.id
JOIN Area AS a ON ho.area_id = a.id
LEFT OUTER JOIN History AS hi
ON hi.person_id = ho.person_id
AND hi.area_id = ho.area_id
AND hi.type = 'I'
AND hi.datetime > ho.datetime
WHERE ho.type = 'O'
AND hi.person_id IS NULL;
All SQL unverified.
You're looking for results where each row may have a different number of columns? I think you may want to look into GROUP_CONCAT()
SELECT p.`id`, GROUP_CONCAT(a.`number`, ',') AS `areas` FROM `person` a LEFT JOIN `history` h ON h.`person_id` = p.`id` LEFT JOIN `area` a ON a.`id` = h.`area_id`
I haven't tested this query, but I have used group concat in similar ways before. Naturally, you will want to tailor this to fit your needs. Of course, group concat will return a string so it will require post processing to use the data.
EDIT I thikn your question has been edited since I began responding. My query does not really fit your request anymore...
Try this:
select *
from person p
inner join history h on h.person_id = p.id
left outer join history h2 on h2.person_id = p.id and h2.area_id = h.area_id and h2.type = 'O'
inner join areas on a.id = h.area_id
where h2.person_id is null and h.type = 'I'