Search code examples
phpmysqljoomlaentity-attribute-value

MySQL EAV SELECT one-to-many single row results


I've seen similar questions on here, but haven't been able to find one that matches my specific scenario. I have the following three tables:

content
id, [other fields]

attributes
id, title

attributes_values
id, attribute_id[foreign to attributes.id], content_id[foreign to content.id], value

What I'd like is a single query (or group of sub-queries) that can return the appropriate data to avoid having to manipulate this in the programming.

A little more info about the tables: attributes has three records (Model #, Part #, and Show in Cart).

I need to select all records from content where the attribute_value for Show in Cart = 1. Along with this, I'd like the other related attribute_values to be returned as their related attribute.title. The results would look something like this:

id    [other fields]    Model # [from attributes.title field]    Part # [from attributes.title field]
1     [any data]        value from attributes_values             value from attributes_values
2     [any data]        value from attributes_values             value from attributes_value

So far, my only way of accomplishing this is not very elegant as I'm having to do multiple joins to the same table:

SELECT * FROM content AS a
LEFT JOIN attributes_values AS b ON (b.content_id = a.id AND b.attribute_id = [Model # ID])
LEFT JOIN attributes_values AS c ON (c.content_id = a.id AND c.attribute_id = [Part # ID])
WHERE a.id IN (
    SELECT content_id FROM attributes_values WHERE attribute_id = [Show in Cart ID] AND value = 1
)

As you can see, I'm having to hard-code related keys into the JOIN statements, which doesn't make this very scalable (and just feels dirty).

Some last few notes: I'm using PHP and Joomla!, so if that would affect your answer (or if there's some secret Joomla tip), feel free to include it. Also, this is a pre-established table schema that I'm not able to alter, so I need a query solution for the above mentioned tables, not a suggestion on a better table design.

Any help will be greatly appreciated.

Cheers!


Solution

  • Instead of joining your table multiple times for each attribute, you can use an aggregated function like MAX() in combination with a CASE WHEN statement:

    SELECT
      content.id,
      [other_fields],
      MAX(CASE WHEN attributes.title='Model #' THEN attributes_values.value END) AS Model_N,
      MAX(CASE WHEN attributes.title='Part #' THEN attributes_values.value END) AS Part_N
    FROM
      content INNER JOIN attributes_values
      ON content.id = attributes_values.content_id
      INNER JOIN attributes
      ON attributes_values.attribute_id = attributes.id
    GROUP BY
      id,
      [other_fields]
    HAVING
      MAX(CASE WHEN attributes.title='Show in Cart' THEN attribute_values.value END)='1'
    

    a little explanation:

    CASE WHEN attributes.title='Model #' THEN attributes.value END
    

    will return the value when the attribute is 'Model #' and NULL otherwise, and

    MAX(...)
    

    will return the maximum non-NULL value, which is the value where attributes.title='Model #'.

    dynamic attributes

    MySQL does not have a Pivot function, so if you want your list to be dynamic, you have to dinamically create a SQL string with all attributes and execute this string.

    You can fetch all attributes from the attributes table:

    SELECT
        CONCAT(
          'MAX(CASE WHEN attributes.title=''',
          REPLACE(attributes.title, '''', ''''''),
          ''' THEN attributes_values.value END) AS `',
          REPLACE(attributes.title, '`', '``'),
          '`'
        )
    FROM
      attributes;
    

    and combine everything together with GROUP_CONCAT:

    SELECT GROUP_CONCAT(...the concat above...)
    FROM attributes;
    

    so your final query will be something like this:

    SELECT
      CONCAT('SELECT content.id,',
    
      GROUP_CONCAT(
        CONCAT(
          'MAX(CASE WHEN attributes.title=''',
          REPLACE(attributes.title, '''', ''''''),
          ''' THEN attributes_values.value END) AS `',
          REPLACE(attributes.title, '`', '``'),
          '`'
        )
      ),
    
      ' FROM content INNER JOIN attributes_values ',
      'ON content.id = attributes_values.content_id ',
      'INNER JOIN attributes ',
      'ON attributes_values.attribute_id = attributes.id ',
      'GROUP BY id HAVING ',
      'MAX(CASE WHEN attributes.title=''Show in Cart'' THEN attributes_values.value END)=''1'''
      )
    FROM
      attributes
    INTO @SQL;
    

    now the @SQL variable will hold the value of the query to be executed, and you can execute it with:

    PREPARE stmt FROM @sql;
    EXECUTE stmt;
    

    The only problem here is that GROUP_CONCAT has a maximum length limit, you can increase it if your table is holding many attributes.

    Please see an example fiddle here.