Search code examples
sqlsql-serverjoincommon-table-expressionentity-attribute-value

SQL query to reconstruct inherited EAV model


I have 5 tables in my database representing an inherited EAV model:

CREATE TABLE AttributeNames
    ("ID" int, "Name" varchar(8))
;

INSERT INTO AttributeNames
    ("ID", "Name")
VALUES
    (1, 'Color'),
    (2, 'FuelType'),
    (3, 'Doors'),
    (4, 'Price')
;

CREATE TABLE MasterCars
    ("ID" int, "Name" varchar(10))
;

INSERT INTO MasterCars
    ("ID", "Name")
VALUES
    (5, 'BMW'),
    (6, 'Audi'),
    (7, 'Ford')
;

CREATE TABLE MasterCarAttributes
    ("ID" int, "AttributeNameId" int, "Value" varchar(10), "MasterCarId" int)
;

INSERT INTO MasterCarAttributes
    ("ID", "AttributeNameId", "Value", "MasterCarId")
VALUES
    (100, 1, 'Red', 5),
    (101, 2, 'Gas', 5),
    (102, 3, '4', 5),
    (102, 4, '$100K', 5),
    (103, 1, 'Blue', 6),
    (104, 2, 'Diesel', 6),
    (105, 3, '3', 6),
    (106, 4, '$80k', 6),
    (107, 1, 'Green', 7),
    (108, 2, 'Diesel', 7),
    (109, 3, '5', 7),
    (110, 4, '$60k', 7)
;

CREATE TABLE LocalCars
    ("ID" int, "MasterCarId" int)
;

INSERT INTO LocalCars
    ("ID", "MasterCarId")
VALUES
    (8, '5'),
    (9, '6'),
    (10, NULL)
;

CREATE TABLE LocalCarAttributes
    ("ID" int, "AttributeNameId" int, "Value" varchar(6), "LocalCarId" int)
;

INSERT INTO LocalCarAttributes
    ("ID", "AttributeNameId", "Value", "LocalCarId")
VALUES
    (43, 1, 'Yellow', 8),
    (44, 3, '6', 9),
    (45, 1, 'Red', 10),
    (46, 2, 'Gas', 10),
    (47, 3, '2', 10),
    (48, 4, '$60k', 10)
;

I can retrieve all of master car attributes as follows:

SELECT MC.ID, MCA.AttributeNameId, MCA.Value
FROM MasterCars MC
left join MasterCarAttributes MCA on MC.ID = MCA.MasterCarId
order by MC.ID;

Likewise, I can retrieve all of the local car attributes as follows:

SELECT LC.ID, LCA.AttributeNameId, LCA.Value
FROM LocalCars LC
left join LocalCarAttributes LCA on LC.ID = LCA.LocalCarId
order by LC.ID;

If LocalCars.MasterCarId is not NULL, then that local car can inherit the attributes of that master car. A local car attribute with the same AttributeNameId overrides any master attribute with the same AttributeNameId.

So given the data above, I have 3 local cars each with 4 attributes (color, fuelType, doors, price). Inherited attribute values in bold:

Local Car Id = 1 (Yellow, Gas, 4, $100K)

Local Car Id = 2 (Blue, Diesel, 6, $80k)

Local Car Id = 3 (Red, Gas, 2, $60k)

I'm trying to find the necessary joins required to join the two queries above together to give a complete set of local cars attributes, some inherited:

LocalCarId    AttributeNameId     Value
------------------------------------------
1             1                   Yellow
1             2                   Gas
1             3                   4
1             4                   $100K
2             1                   Blue
2             2                   Diesel
2             3                   6
2             4                   $80K
3             1                   Red
3             2                   Gas
3             3                   2
3             4                   $60K

or possibly even:

LocalCarId    AttributeNameId     LocalValue         MasterValue
    -------------------------------------------------------------
    1             1                   Yellow        Red
    1             2                   NULL          Gas
    1             3                   NULL          4
    1             4                   NULL          $100K
    2             1                   NULL          Blue
    2             2                   NULL          Diesel
    2             3                   6             3
    2             4                   NULL          $80K
    3             1                   Red           NULL
    3             2                   Gas           NULL
    3             3                   2             NULL
    3             4                   $60K          NULL

Solution

  • The problem can be solved by performing a union on all of your local car attributes and master car attributes. Each record is marked with an [IsMasterAttribute] flag. The next step is then use the ROW_NUMBER() window function to rank each of the duplicate attributes. The final step is to only select attributes which has a rank of 1.

        ;WITH CTE_CombinedAttributes
        AS
        (
            SELECT               1 AS IsMasterAttribute
                                ,LC.ID
                                ,MC.ID AS MasterCarId
                                ,MCA.AttributeNameId
                                ,MCA.Value
            FROM                 MasterCars MC
            LEFT OUTER JOIN     MasterCarAttributes MCA on MC.ID = MCA.MasterCarId
            INNER JOIN          LocalCars LC ON LC.MasterCarId = MC.ID
            UNION ALL
            SELECT               0 AS IsMasterAttribute
                                ,LC.ID
                                ,LC.MasterCarId
                                ,LCA.AttributeNameId
                                ,LCA.Value
            FROM                LocalCars LC
            LEFT OUTER JOIN     LocalCarAttributes LCA on LC.ID = LCA.LocalCarId
        )
        , 
        CTE_RankedAttributes
        AS
        (
            SELECT   [IsMasterAttribute]
                    ,[ID]
                    ,[AttributeNameId]
                    ,[Value]
                    ,ROW_NUMBER() OVER (PARTITION BY [ID], [AttributeNameId] ORDER BY [IsMasterAttribute]) AS [AttributeRank]
            FROM    CTE_CombinedAttributes
        )
        SELECT       [IsMasterAttribute]
                    ,[ID]
                    ,[AttributeNameId]
                    ,[Value]
        FROM        CTE_RankedAttributes
        WHERE       [AttributeRank] = 1
        ORDER BY    [ID]
    

    The second output is also possible by performing a simple pivot on the final result:

        ;WITH CTE_CombinedAttributes
        AS
        (
            SELECT               1 AS IsMasterAttribute
                                ,LC.ID
                                ,MC.ID AS MasterCarId
                                ,MCA.AttributeNameId
                                ,MCA.Value
            FROM                 MasterCars MC
            LEFT OUTER JOIN     MasterCarAttributes MCA on MC.ID = MCA.MasterCarId
            INNER JOIN          LocalCars LC ON LC.MasterCarId = MC.ID
            UNION ALL
            SELECT               0 AS IsMasterAttribute
                                ,LC.ID
                                ,LC.MasterCarId
                                ,LCA.AttributeNameId
                                ,LCA.Value
            FROM                LocalCars LC
            LEFT OUTER JOIN     LocalCarAttributes LCA on LC.ID = LCA.LocalCarId
        )
        , 
        CTE_RankedAttributes
        AS
        (
            SELECT   [IsMasterAttribute]
                    ,[ID]
                    ,[AttributeNameId]
                    ,[Value]
                    ,ROW_NUMBER() OVER (PARTITION BY [ID], [AttributeNameId] ORDER BY [IsMasterAttribute]) AS [AttributeRank]
            FROM    CTE_CombinedAttributes
        )
        SELECT       [ID]
                    ,[AttributeNameId]
                    ,MAX(
                        CASE [IsMasterAttribute]
                            WHEN 0 THEN [Value]
                        END
                     ) AS LocalValue
                    ,MAX(
                        CASE [IsMasterAttribute]
                            WHEN 1 THEN [Value]
                        END
                     ) AS MasterValue
        FROM        CTE_RankedAttributes
        GROUP BY    [ID], [AttributeNameId]
        ORDER BY    [ID]