Receiving 1 row from joined (1 to many) postgresql - sql

I have this problem:
I have 2 major tables (apartments, tenants) that have a connection of 1 to many (1 apartment, many tenants).
I'm trying to pull all my building apartments, but with one of his tenants.
The preffered tenant is the one who have ot=2 (there are 2 possible values: 1 or 2).
I tried to use subqueries but in postgresql it doesn't let you return more than 1 column.
I don't know how to solve it. Here is my latest code:
SELECT a.apartment_id, a.apartment_num, a.floor, at.app_type_desc_he, tn.otype_desc_he, tn.e_name
FROM
public.apartments a INNER JOIN public.apartment_types at ON
at.app_type_id = a.apartment_type INNER JOIN
(select t.apartment_id, t.building_id, ot.otype_id, ot.otype_desc_he, e.e_name
from public.tenants t INNER JOIN public.ownership_types ot ON
ot.otype_id = t.ownership_type INNER JOIN entities e ON
t.entity_id = e.entity_id
) tn ON
a.apartment_id = tn.apartment_id AND tn.building_id = a.building_id
WHERE
a.building_id = 4 AND tn.building_id=4
ORDER BY
a.apartment_num ASC,
tn.otype_id DESC
Thanx in advance

SELECT a.apartment_id, a.apartment_num, a.floor
,at.app_type_desc_he, tn.otype_desc_he, tn.e_name
FROM public.apartments a
JOIN public.apartment_types at ON at.app_type_id = a.apartment_type
LEFT JOIN (
SELECT t.apartment_id, t.building_id, ot.otype_id
,ot.otype_desc_he, e.e_name
FROM public.tenants t
JOIN public.ownership_types ot ON ot.otype_id = t.ownership_type
JOIN entities e ON t.entity_id = e.entity_id
ORDER BY (ot.otype_id = 2) DESC
LIMIT 1
) tn ON (tn.apartment_id, tn.building_id)=(a.apartment_id, a.building_id)
WHERE a.building_id = 4
AND tn.building_id = 4
ORDER BY a.apartment_num; -- , tn.otype_id DESC -- pointless
Crucial part emphasized.
This works in either case.
If there are tenants for an apartment, exactly 1 will be returned.
If there is one (or more) tenant of ot.otype_id = 2, it will be one of that type.
If there are no tenants, the apartment is still returned.
If, for ot.otype_id ...
there are 2 possible values: 1 or 2
... you can simplify to:
ORDER BY ot.otype_id DESC
Debug query
Try removing the WHERE clauses from the base query and change
JOIN public.apartment_types
to
LEFT JOIN public.apartment_types
and add them back one by one to see which condition excludes all rows.
Do at.app_type_id and a.apartment_type really match?

Related

How does this SQL query return results with same id_product?

I am facing a complex SQL query in some code, which is suppose to return products without duplicates (by the use of DISTINCT keywork at the beginning), here is the query:
SELECT DISTINCT p.`id_product`, p.*, product_shop.*, pl.* , m.`name` AS manufacturer_name, x.`id_feature` , x.`id_feature_value` , s.`name` AS supplier_name
FROM `ps_product` p
INNER JOIN ps_product_shop product_shop
ON (product_shop.id_product = p.id_product AND product_shop.id_shop = 1)
LEFT JOIN `ps_product_attribute` y ON (y.`id_product` = p.`id_product`)
LEFT JOIN `ps_product_attribute_combination` ac ON (y.`id_product_attribute` = ac.`id_product_attribute`)
LEFT JOIN `ps_product_lang` pl ON (p.`id_product` = pl.`id_product` AND pl.id_shop = 1 )
LEFT JOIN `ps_manufacturer` m ON (m.`id_manufacturer` = p.`id_manufacturer`)
LEFT JOIN `ps_feature_product` x ON (x.`id_product` = p.`id_product`)
LEFT JOIN `ps_supplier` s ON (s.`id_supplier` = p.`id_supplier`)
LEFT JOIN `ps_category_product` c ON (c.`id_product` = p.`id_product`)
WHERE pl.`id_lang` = 1 AND c.`id_category` = 18 AND p.`price` between 0 and 1000
AND product_shop.`visibility` IN ("both", "catalog") AND product_shop.`active` = 1
ORDER BY p.`id_product` ASC LIMIT 1,4
But it returns 4 product with 2 products with same "id_product" (11941)
What I need is to return 4 products but of different ids each.
Anyone ?
Thanks a lot
Aymeric
[EDIT]
The result of this query shows 4 rows, with 2 having the same exact columns values EXCEPT for the id_feature_value column which 36 for one and 38 for the other.
SELECT DISTINCT gets all the distinct combinations of all selected fields in your query, not just the first field.
Now, you could solve that by using GROUP BY to select only distinct values of id_product specifically, like:
SELECT p.`id_product`, p.*, product_shop.*, pl.* , m.`name` AS manufacturer_name, x.`id_feature` , x.`id_feature_value` , s.`name` AS supplier_name
FROM `ps_product` p
INNER JOIN ps_product_shop product_shop
ON (product_shop.id_product = p.id_product AND product_shop.id_shop = 1)
LEFT JOIN `ps_product_attribute` y ON (y.`id_product` = p.`id_product`)
LEFT JOIN `ps_product_attribute_combination` ac ON (y.`id_product_attribute` = ac.`id_product_attribute`)
LEFT JOIN `ps_product_lang` pl ON (p.`id_product` = pl.`id_product` AND pl.id_shop = 1 )
LEFT JOIN `ps_manufacturer` m ON (m.`id_manufacturer` = p.`id_manufacturer`)
LEFT JOIN `ps_feature_product` x ON (x.`id_product` = p.`id_product`)
LEFT JOIN `ps_supplier` s ON (s.`id_supplier` = p.`id_supplier`)
LEFT JOIN `ps_category_product` c ON (c.`id_product` = p.`id_product`)
WHERE pl.`id_lang` = 1 AND c.`id_category` = 18 AND p.`price` between 0 and 1000
AND product_shop.`visibility` IN ("both", "catalog") AND product_shop.`active` = 1
GROUP BY p.`id_product`
ORDER BY p.`id_product` ASC LIMIT 1,4
However, the problem now is that your query has multiple different values of all the other fields you are selected to choose from, and no deterministic way to pick from them. Even though the id_product is unique in it's table, it's not unique in the result set because in at least one of your JOINs there is a one-to-many relationship, meaning there are several rows that match the JOIN conditions.
On older versions of MySQL, it will just pick the first value it finds in this case, but on SQL Server it will actually error out and tell you that the remaining fields either have to be mentioned in the GROUP BY clause, or they have to be aggregated. So, you've got a few ways you can go from here:
You are on an old version of MySQL and you don't particularly care which values are returned for the rest of the fields, so leave the query as I've posted and use that. I wouldn't recommend this, as it's undefined behaviour so in theory it could change at MySQL's whim. All the values returned will be from the same result row though.
Add aggregate functions, such as MIN() or MAX() to the rest of the remaining fields in the select clause. This will reduce the possible values for the fields down to one, but you will probably end up with a mixture of values from different rows.
Remove any one-to-many JOINs from your query so that you only ever get one row back in the result set for each individual id_product. Then, fetch the remaining data you need in a separate query.
There may be other alternative solutions, but it depends a lot on which values you want returned for the rest of the rows and what RDBMS you are using. For example, on SQL Server you could potentially make use of PARTITION BY to select the first row for each distinct id_product deterministically.

SQL Server 2012 -- loop keys

I have written a query which left joins separate tables and attempts to discover at a point in time (the point of insertion) which PK keys are the newest in a db for a given record.
you will see i have pared it back to patientid 100 as this is the only way i can seem to get it working.
The current query works as shown:
SELECT TOP 1 P1.PatientID,
P1.DimPatientPK,
DA1.DimAdmissionPK,
DD1.DiagnosisPK,
DI1.Investigation1PK,
DIE1.InvestigationECGPK,
IEG1.InvestigationEchoGoldPK,
MH1.DimMedicalHistoryPK,
FH1.DimPatientFamilyHistoryPK,
PHT1.PatientHospitalisationTreatmentPK,
PMP1.PatientMedicalPersonnelPK,
RR1.PatientReferralReasonPK,
PEA1.PhysicalExamAHSPK,
PEM1.PhysicalExamMurmursPK,
SI1.SocialIssuePK,
TRT.TreatmentPK
--DT1.Treatment1PK
FROM
DimPatient P1 LEFT JOIN DimAdmission DA1 ON P1.PatientID = DA1.PatientID
LEFT JOIN DimDiagnosis DD1 ON P1.PatientID = DD1.PatientID
LEFT JOIN DimInvestigation1 DI1 ON P1.PatientID = DI1.PatientID
LEFT JOIN DimInvestigationECG DIE1 ON P1.PatientID = DIE1.PatientId
LEFT JOIN DimInvestigationECHOgold IEG1 ON P1.PatientID = DIE1.PatientId
LEFT JOIN DimMedicalHistory MH1 ON P1.PatientID = MH1.PatientId
LEFT JOIN DimPatientFamilyHistory FH1 ON P1.PatientId = FH1.PatientID
LEFT JOIN DimPatientHospitalisationTreatment PHT1 ON P1.PatientID = PHT1.PatientId
LEFT JOIN DimPatientMedicalPersonnel PMP1 ON P1.PatientID = PMP1.PatientId
LEFT JOIN DimPatientReferralReason RR1 ON P1.PatientID = RR1.PatientId
LEFT JOIN DimPhysicalExamAHS PEA1 ON P1.PatientId = PEA1.PatientId
LEFT JOIN DimPhysicalExamination PE1 ON P1.PatientID = PE1.PatientId
LEFT JOIN DimPhysicalExamMurmurs PEM1 ON P1.PatientID = PEM1.PatientId
LEFT JOIN DimSocialIssue SI1 ON P1.PatientID = SI1.PatientID
LEFT JOIN DimTreatment TRT ON P1.PatientID = TRT.PatientId
WHERE P1.patientid IN(100)
ORDER BY DA1.DimAdmissionPK DESC,
P1.DimPatientPK DESC,
DD1.DiagnosisPK DESC,
DI1.Investigation1PK DESC,
DIE1.InvestigationECGPK DESC,
IEG1.InvestigationEchoGoldPK DESC,
MH1.DimMedicalHistoryPK DESC,
FH1.DimPatientFamilyHistoryPK DESC,
PHT1.PatientHospitalisationTreatmentPK DESC,
PMP1.PatientMedicalPersonnelPK DESC,
RR1.PatientReferralReasonPK DESC,
PEA1.PhysicalExamAHSPK DESC,
PE1.PhysicalExaminationPK DESC,
PEM1.PhysicalExamMurmursPK DESC,
SI1.SocialIssuePK DESC,
TRT.TreatmentPK DESC;
This successfully recovers a full record whether it has been filled out or not for the patid 100.
I am having trouble expanding this so that it loops through and collects the same results for every patient in the db.
i.e. if i remove the where clause, i only get 1 row still ..
if i remove select top 1 .. then it returns me multiple sets of patientid 90 - i basically want 1 row for each patientID - ie 90, 91, 92 with the corresponding maximum key value from each table matched.
Anyone have any ideas on how to achieve this?
One (or more) tables you are joining is empty. Change your joins to left outer join, i.e.
LEFT OUTER JOIN DimAdmission DA1 ON P1.PatientID = DA1.PatientID
for all joined tables. The empty table will have Nulls in the results set columns.
If multiple selections for the same patient ID exist, then one or more of your tables has more than one record for each patient ID. You either need to exclude these tables from the query or look at your data structure and see if there is a field which will let you select single record.
I suggest you add the ROW_NUMBER function into your WHERE clause, e.g.
WHERE ROW_NUMBER() (PARTION BY P1.patientid ORDER BY DA1.DimAdmissionPK desc,
P1.DimPatientPK desc, ... ) = 1
By "..." I mean move your entire ORDER BY clause inside the ROW_NUMBER function.
Good luck - it seems you are missing a Fact table ...

Joining top records in T-SQL

SELECT MD.*, Contact.FirstName
FROM MerchantData MD
JOIN Merchant M ON M.MerchID = MD.MerchID
JOIN (SELECT TOP 1 * FROM Location WHERE Location.BusID = MD.BusID) L ON L.BusID=MD.BusID
AND L.Deleted = 0
JOIN Contact ON Contact.ContactID = L.PrincipalID
I am using SQLSERVER 2008 and trying to write this SQL statement. There is some times multiple locations for a busid and I want to join in only the first found. I am getting an error on the part "Location.BusID = MD.BusID" as MD.BusID cannot be bound. Is it possible to use the MD table in the nested select statment in this join or is there another way of accomplishing this?
I am contiplating putting the data using nested querys in the column list to grab the contact data driectly there.
It would be simpler I think to have a subquery of the full result set:
SELECT MD.*, Contact.FirstName
FROM MerchantData MD
JOIN Merchant M ON M.MerchID = MD.MerchID
JOIN (SELECT BusID, MAX(PrincipalID)
FROM Location
WHERE Deleted = 0
GROUP BY BusID) L ON L.BusID=MD.BusID
JOIN Contact ON Contact.ContactID = L.PrincipalID
You still get one record per BusID in the JOIN but it's not correlated.
SELECT MD.*, Contact.FirstName
FROM MerchantData MD
JOIN Merchant M ON M.MerchID = MD.MerchID
CROSS APPLY (SELECT TOP 1 * FROM Location WHERE BusID = MD.BusID AND DELETED = 0) L
JOIN Contact ON Contact.ContactID = L.PrincipalID
This is a case of the "top n per group" problem. This question will guide you:
SQL Server query select 1 from each sub-group
You'll want to be doing something like this:
SELECT MD.* ,
Contact.FirstName
FROM MerchantData MD
JOIN Merchant M ON M.MerchID = MD.MerchID
JOIN ( select * ,
seq = rank() over( partition by BusID order by BusID , ... )
from Location
where L.Deleted = 0
) L on L.BusID = MD.BusID
and seq = 1
JOIN Contact ON Contact.ContactID = L.PrincipalID
The virtual table expression should return at most 1 Location per BusID (0 if the BusID has no non-deleted Locations).
To try and isolate out the error I would try. See if it can match Location.BusID = MD.BusID.
SELECT MD.*, Contact.FirstName
FROM MerchantData MD
JOIN Merchant M ON M.MerchID = MD.MerchID
JOIN Location On Location.BusID = MD.BusID
You do not use the * so use
SELECT TOP 1 Location.BusID FROM Location WHERE Location.BusID = MD.BusID
Once you get the syntax working.
You do know that once you get this working it will only match if the "first" row and then check if it is deleted. The problem is that without an order by the "first" row is arbitrary. Even with a clustered index on a table there is no guaranteed sort without an order by clause. To get a repeatable answer you need a sort. But if you are sorting and only want the top row then a MAX or MIN and a group be more straight forward.
If you want just business that have one or more deleted locations then the following should work but you need to break out the columns for the group by. If two deleted locations have differenct contact name then it will report each. So, this may not be what you are looking for.
SELECT MD.col1, MD.col2, Contact.FirstName
FROM MerchantData MD
JOIN Merchant M ON M.MerchID = MD.MerchID
JOIN Location L
ON L.BusID = MD.BusID
AND L.Deleted = 0
JOIN Contact ON Contact.ContactID = L.PrincipalID
GROUP BY MD.col1, MD.col2, Contact.FirstName

SQL joins "going up" two tables

I'm trying to create a moderately complex query with joins:
SELECT `history`.`id`,
`parts`.`type_id`,
`serialized_parts`.`serial`,
`history_actions`.`action`,
`history`.`date_added`
FROM `history_actions`, `history`
LEFT OUTER JOIN `parts` ON `parts`.`id` = `history`.`part_id`
LEFT OUTER JOIN `serialized_parts` ON `serialized_parts`.`parts_id` = `history`.`part_id`
WHERE `history_actions`.`id` = `history`.`action_id`
AND `history`.`unit_id` = '1'
ORDER BY `history`.`id` DESC
I'd like to replace `parts`.`type_id` in the SELECT statement with `part_list`.`name` where the relationship I need to enforce between the two tables is `part_list`.`id` = `parts`.`type_id`. Also I have to use joins because in some cases `history`.`part_id` may be NULL which obviously isn't a valid part id. How would I modify the query to do this?
Here is some sample date as requested:
history table:
(source: ianburris.com)
serialized_parts table:
(source: ianburris.com)
parts table:
(source: ianburris.com)
part_list table:
(source: ianburris.com)
And what I want to see is:
id name serial action date_added
4 Battery 567 added 2010-05-19 10:42:51
3 Antenna Board 345 added 2010-05-19 10:42:51
2 Main Board 123 added 2010-05-19 10:42:51
1 NULL NULL created 2010-05-19 10:42:51
This would at least be on the right track...
If you're looking to NOT show any parts with an invalid ID, simply change the LEFT JOINs to INNER JOINs (they will restrict NULL values)
SELECT `history`.`id`
, `parts`.`type_id`
, `part_list`.`name`
, `serialized_parts`.`serial`
, `history_actions`.`action`
, `history`.`date_added`
FROM `history_actions`
INNER JOIN `history` ON `history`.`action_id` = `history_actions`.`id`
LEFT JOIN `parts` ON `parts`.`id` = `history`.`part_id`
LEFT JOIN `serialized_parts` ON `serialized_parts`.`parts_id` = `history`.`part_id`
LEFT JOIN `part_list` ON `part_list`.`id` = `parts`.`type_id`
WHERE `history`.`unit_id` = '1'
ORDER BY `history`.`id` DESC
Boy, these backticks make my eyes hurt.
SELECT
h.id,
p.type_id,
pl.name,
sp.serial,
ha.action,
h.date_added
FROM
history h
INNER JOIN history_actions ha ON ha.id = h.action_id
LEFT JOIN parts p ON p.id = h.part_id
LEFT JOIN serialized_parts sp ON sp.parts_id = h.part_id
LEFT JOIN part_list pl ON pl.id = p.type_id
WHERE
h.unit_id = '1'
ORDER BY
history.id DESC

dynamic sql pivot table

i hope you can help me with the resolution of this task we have.
originally we have these tables:
hwtype
id name
1 router
2 switch
hwelement
id idhwtype name
1 1 RTR1
2 1 RTR2
3 2 SWT1
hwattributes
id idhwtype name
1 1 speed
2 1 IP
3 2 ports
hwtypeattributes
id idhwelement idhwattribute value
1 1 1 100mb
2 1 2 172.16.3.23
3 2 1 10mb
4 2 2 172.16.3.26
5 3 3 8
what we need now is a function that presents the data in this way (according hwtype )
for hwtype.name =router
element speed IP
RTR1 100mb 172.16.3.23
RTR2 10mb 172.16.3.26
The idea is to make the tables able to include new element types, elements and attributes without having to modify the tables coding.
I had been looking for examples but unfortunately i had found good ones that do aggregation on values which is something i had not consider.
thanks in advance for your help
You're using the EAV antipattern. This breaks all sorts of rules of relational database design and as you have discovered, getting data out is very awkward. There are many other weaknesses of this design, recounted elsewhere.
Read the article "Bad CaRMa" for a great story of how an EAV system destroyed a company.
Here's what you have to do to get the router attributes out of your database:
SELECT e.name AS "element",
speedval.value AS "speed",
ipval.value AS "IP",
portsval.value AS "Ports"
FROM hwtype t
JOIN hwelement e ON (e.idhwtype = t.id)
JOIN hwattributes speed ON (speed.idhwtype = t.id AND speed.name = 'speed')
LEFT OUTER JOIN hwtypeattributes speedval
ON (speedval.idhwattribute = speed.id AND speedval.idhwelement = e.id)
JOIN hwattributes ip ON (ip.idhwtype = t.id AND ip.name = 'ip')
LEFT OUTER JOIN hwtypeattributes ipval
ON (ipval.idhwattribute = ip.id AND ipval.idhwelement = e.id)
JOIN hwattributes ports ON (ports.idhwtype = t.id AND ports.name = 'ports')
LEFT OUTER JOIN hwtypeattributes portsval
ON (portsval.idhwattribute = ports.id AND portsval.idhwelement = e.id)
WHERE t.name = 'router';
Note that you need an extra pair of joins for each attribute if you insist on fetching all attributes for a given element on a single row. This quickly gets prohibitively expensive for the SQL optimizer.
It's far easier to fetch the attributes on multiple rows, and sort it out in application code:
SELECT e.name AS "element", a.name, v.value
FROM hwtype t
JOIN hwelement e ON (e.idhwtype = t.id)
JOIN hwattributes a ON (a.idhwtype = t.id)
JOIN hwtypeattributes v ON (v.idhwattribute = a.id AND v.idhwelement = e.id)
WHERE t.name = 'router';