Search code examples
mysqlselectjoinunionentity-attribute-value

Query by Value in Adjacent Column in MySQL


I believe all questions of this nature start out this way... I have two tables: fields and data. Fields describes the column names of a (non-existant) table and data contains the data of that (non-existant) table. Like so...

Fields:

    +----+---------+-------------+---------------+-------+----------+
    | ID | F_ORDER | NAME        | LABEL         | VALUE | TYPE     |
    +----+---------+-------------+---------------+-------+----------+
    |  1 |       1 | IS_EMPLOYEE | Region        |       | checkbox |
    |  2 |       3 | EM_AVATAR   | Avatar        |       | avatar   |
    |  3 |       4 | EM_JOBTITLE | Job Title     |       | text     |
    |  4 |       5 | EM_COMPANY  | Company       |       | text     |
    |  5 |       6 | EM_PHONE    | Phone         |       | text     |
    |  6 |       2 | EM_ORDER    | Display Order | 5     | text     |
    +----+---------+-------------+---------------+-------+----------+

Data:

    +-----+----------+---------+--------------------------------------+
    | ID  | FIELD_ID | USER_ID | VALUE                                |
    +-----+----------+---------+--------------------------------------+
    |   5 |        1 |       1 | YES                                  |
    |   6 |        2 |       1 |                                      |
    |   7 |        3 |       1 | Owner                                |
    |   8 |        4 |       1 | Acme, Inc.                           |
    |   9 |        5 |       1 | 123-456-7987                         |
    | 150 |        5 |      31 | 123-623-5555                         |
    | 149 |        4 |      31 | Acme, Inc.                           |
    | 148 |        3 |      31 | Sales and Customer Support           |
    | 147 |        2 |      31 |                                      |
    | 146 |        1 |      31 | YES                                  |
    |  26 |        1 |       6 | NO                                   |
    |  27 |        2 |       6 | http://example.com/avi/avi.jpeg      |
    |  28 |        3 |       6 | CEO                                  |
    |  29 |        4 |       6 | Acme                                 |
    |  30 |        5 |       6 | (123) 734-5555                       |
    |  31 |        1 |       7 | NO                                   |
    |  32 |        2 |       7 | http://example.com/avi/avi.jpeg      |
    |  33 |        3 |       7 | VP, Services                         |
    |  34 |        4 |       7 | Acme                                 |
    |  35 |        5 |       7 | (913) 963-5555                       |
    |  36 |        1 |      14 | NO                                   |
    |  37 |        2 |      14 | http://example.com/avi/avi.jpeg      |
    |  38 |        3 |      14 | Senior Accountant                    |
    |  39 |        4 |      14 | Acme                                 |
    |  40 |        5 |      14 | (123) 213-5555                       |
    |  41 |        1 |      10 | NO                                   |
    |  42 |        2 |      10 | http://example.com/avi/avi.jpeg      |
    |  43 |        3 |      10 | President                            |
    |  44 |        4 |      10 | Acme                                 |
    |  45 |        5 |      10 | (123) 734-5555                       |
    |  46 |        1 |      12 | NO                                   |
    |  47 |        2 |      12 | http://example.com/avi/avi.jpeg      |
    |  48 |        3 |      12 | Services Supervisor                  |
    |  49 |        4 |      12 | Acme                                 |
    |  50 |        5 |      12 | (123) 573-5555                       |
    |  51 |        1 |      11 | NO                                   |
    |  52 |        2 |      11 | http://example.com/avi/avi.jpeg      |
    |  53 |        3 |      11 | Operations Supervisor                |
    |  54 |        4 |      11 | Acme                                 |
    |  55 |        5 |      11 | (123) 259-5555                       |
    |  56 |        1 |       8 | NO                                   |
    |  57 |        2 |       8 | http://example.com/avi/avi.jpeg      |
    |  58 |        3 |       8 | General Information                  |
    |  59 |        4 |       8 | Acme                                 |
    |  60 |        5 |       8 | (123) 213-5555                       |
    |  61 |        1 |       9 | NO                                   |
    |  62 |        2 |       9 | http://example.com/avi/avi.jpeg      |
    |  63 |        3 |       9 | VP, Sales                            |
    |  64 |        4 |       9 | Acme                                 |
    |  65 |        5 |       9 | (123) 210-5555                       |
    +-----+----------+---------+--------------------------------------+

The basic verbiage of the query I'm looking for is: I want all information for all people who are employees (IS_EMPLOYEE = "YES") and ordered by their display order column (EM_ORDER).

My query so far gets me nowhere. I get results a little like this:

    +--------+------------+------------+-------+---------+-------+
     ID        FIELD_ID    NAME         LABEL   TYPE       VALUE
    +--------+------------+------------+-------+---------+-------+
     7         1           IS_EMPLOYEE  Region  checkbox   YES
    +--------+------------+------------+-------+---------+-------+

What I need are results like this:

    +-------+------------+---------+-----------+----------+-------------+--------+
     USER_ID IS_EMPLOYEE  EM_AVATAR EM_JOBTITLE EM_COMPANY EM_PHONE      EM_ORDER   
    +-------+------------+---------+-----------+----------+-------------+--------+
     6       YES           http://  CEO         Acme       123-123-555   5 
    +-------+------------+---------+-----------+----------+-------------+--------+ 

And of course I'm trying to get it all back into PHP as a usable array ($results['user_id']['jobtitle'] etc.). I could just get everything and work through it with PHP, but I'm trying to learn MySQL and I think this is a faster method than doing several foreach blocks ... although I could be wrong.

Thanks in advance for any help.


Solution

  • This is basically a PIVOT but MySQL does not have a PIVOT function so you can replicate it using an aggregate function and a CASE statement.

    A Static version is when you know all of the values that you want to transform into columns (these are your field names):

    select 
      d.user_id,
      max(case when f.name = 'IS_EMPLOYEE' then d.value else null end) IS_EMPLOYEE,
      max(case when f.name = 'EM_AVATAR' then d.value else null end) EM_AVATAR,
      max(case when f.name = 'EM_JOBTITLE' then d.value else null end) EM_JOBTITLE,
      max(case when f.name = 'EM_COMPANY' then d.value else null end) EM_COMPANY,
      max(case when f.name = 'EM_PHONE' then d.value else null end) EM_PHONE,
      max(case when f.name = 'EM_ORDER' then d.value else null end) EM_ORDER
    from data d
    left join fields f
      on f.id = d.FIELD_ID
    group by d.user_id
    

    See SQL Fiddle with Demo

    Result:

    | USER_ID | IS_EMPLOYEE |                       EM_AVATAR |                EM_JOBTITLE | EM_COMPANY |       EM_PHONE | EM_ORDER |
    ---------------------------------------------------------------------------------------------------------------------------------
    |       1 |         YES |                          (null) |                      Owner | Acme, Inc. |   123-456-7987 |   (null) |
    |       6 |          NO | http://example.com/avi/avi.jpeg |                        CEO |       Acme | (123) 734-5555 |   (null) |
    |       7 |          NO | http://example.com/avi/avi.jpeg |               VP, Services |       Acme | (913) 963-5555 |   (null) |
    |       8 |          NO | http://example.com/avi/avi.jpeg |        General Information |       Acme | (123) 213-5555 |   (null) |
    |       9 |          NO | http://example.com/avi/avi.jpeg |                  VP, Sales |       Acme | (123) 210-5555 |   (null) |
    |      10 |          NO | http://example.com/avi/avi.jpeg |                  President |       Acme | (123) 734-5555 |   (null) |
    |      11 |          NO | http://example.com/avi/avi.jpeg |      Operations Supervisor |       Acme | (123) 259-5555 |   (null) |
    |      12 |          NO | http://example.com/avi/avi.jpeg |        Services Supervisor |       Acme | (123) 573-5555 |   (null) |
    |      14 |          NO | http://example.com/avi/avi.jpeg |          Senior Accountant |       Acme | (123) 213-5555 |   (null) |
    |      31 |         YES |                          (null) | Sales and Customer Support | Acme, Inc. |   123-623-5555 |   (null) |
    

    If you have an unknown number of values to turn into columns, then you can use a prepared statement to generate the sql dynamically.

    SET @sql = NULL;
    SELECT
      GROUP_CONCAT(DISTINCT
        CONCAT(
          'max(case when f.name = ''',
          name,
          ''' then d.value end) AS ',
          name
        )
      ) INTO @sql
    FROM fields;
    
    SET @sql = CONCAT('SELECT d.user_id, ', @sql, ' 
                      from data d
                      left join fields f
                        on f.id = d.FIELD_ID
                       GROUP BY d.user_id');
    
    PREPARE stmt FROM @sql;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
    

    See SQL Fiddle with Demo

    Both will produce the same result.

    You can then add a WHERE clause to filter out any of the unneeded rows.