简体   繁体   中英

JOIN on varchar column with subquery

i know that this is not the recommended way to join tables. But it's only relevant for one rarely used report for one person and i don't want to change my datamodel for it.

I have two tables Model and SparePart that are not directly linked with each other via foreign keys.

Model            SparePart
idModel          idSparePart
ModelName        SparePartDescription
                 Price

In special cases a model is also a sparepart(exchange unit). Then i need the price for this model from the SparePart table via its SparePartDescription column.

For example:

ModelName = C510
SparePartDescription = C510/Exchange Unit/Exch unit/Red

So i try to join both tables to get the price with following SQL:

SELECT m.idModel, m.ModelName, sp.Price, sp.SparePartDescription
FROM   modModel AS m INNER JOIN 
       tabSparePart AS sp ON m.ModelName =
       (SELECT TOP 1 LEFT(sp.SparePartDescription, CHARINDEX('/', sp.SparePartDescription) - 1)order by price desc)
WHERE  (CHARINDEX('/', sp.SparePartDescription) > 0) 
AND  (sp.fiSparePartCategory = 6)
ORDER BY m.ModelName, sp.SparePartDescription

But i get multiple records for one model:

idModel    ModelName   Price        SparePartDescription
569        C510        70,75        C510/Exchange Unit/Exch unit/Red
569        C510        70,75        C510/Exchange Unit/Latin/Generic/Black
569        C510        70,75        C510/Exchange Unit/Latin/Generic/Silver
433        C702        80,72        C702/Exchange Unit/Latin/Generic/Black
433        C702        NULL         C702/Exchange Unit/Latin/Generic/Cyan
433        C702        80,72        C702/Exchange Unit/Orange Global/Black

I only want to select one record if there are multiple spareparts with matching SparePartDescription.

Sql Server 2005 and better introduced the 'APPLY' operator which allows you to join against a subquery... Try this.

SELECT m.idModel, m.ModelName, sp.Price, sp.SparePartDescription
FROM   modModel AS m 
CROSS APPLY
(
  SELECT TOP 1 * FROM tabSparePart
  WHERE m.ModelName = 
   LEFT(SparePartDescription, LEN(ModelName)) 
  ORDER BY Price DESC
) sp
WHERE  (sp.fiSparePartCategory = 6)
ORDER BY m.ModelName, sp.SparePartDescription

It inner joins the 'modModel' table with the subquery 'only the top one matching tabSparePart'.

You can also use OUTER APPLY which will emulate a LEFT JOIN on the subquery. Documentation is here .

First, your join condition can be simplified some, and then you can use ROW_NUMBER() to specify some kind of order to your results, allowing the first result (per model) to be selected. I also changed it to a LEFT JOIN in case there was no match. If that is not required, it's simple to change back to an INNER JOIN :)

WITH
  ranked_results AS
(
  SELECT
    m.idModel, m.ModelName, sp.Price, sp.SparePartDescription,
    ROW_NUMBER() OVER (PARTITION BY m.idModel ORDER BY sp.Price DESC) AS rank
  FROM
    modModel       AS m
  LEFT JOIN 
    tabSparePart   AS sp
      ON  LEFT(sp.SparePartDescription, LEN(m.ModelName)) = m.ModelName
      AND (CHARINDEX('/', sp.SparePartDescription) > 0) 
      AND (sp.fiSparePartCategory = 6)
)
SELECT
  *
FROM
  ranked_results
WHERE
  rank = 1
ORDER BY
  ModelName,
  SparePartDescription


@MattMurrell's answer just appeared while I was typing this. One difference here is that the selection criteria is being applied to the whole set, rather than separately in the CROSS APPLY. This may have a performance benefit, you'd have to try and see. CROSS APPLY with inline functions is normally more performant that correlated sub-queries, so I can't predict which is faster.

Try the ROW_NUMBER function. It guarantees that you'll only get one of each item as defined in the PARTITION BY clause.

SELECT a.idModel, a.ModelName, Price, SparePartDescription
FROM modModel a
LEFT JOIN
(
    SELECT m.idModel, m.ModelName, sp.Price, sp.SparePartDescription
        , ROW_NUMBER() OVER (PARTITION BY m.idModel, m.ModelName ORDER BY sp.price DESC) AS r
    FROM   modModel AS m 
    INNER JOIN tabSparePart AS sp 
        ON m.ModelName = LEFT(sp.SparePartDescription, CHARINDEX('/', sp.SparePartDescription) - 1)
    WHERE  (CHARINDEX('/', sp.SparePartDescription) > 0) 
        AND  (sp.fiSparePartCategory = 6)
) b
    ON a.idModel = b.idModel
    AND b.r = 1
ORDER BY ModelName, SparePartDescription

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM