skip to Main Content

I have implemented an EAV model using MySQL (phpmyadmin) for an e-commerce website developed with CodeIgniter Framework (PHP).
The EAV Model goes this way:

Table: Products

id | name   | description
-------------------------
1  | prod_1 | lorem
2  | prod_2 | ipsum

Table: Attributes

id | name   | description
-------------------------
1  | attr_1 | dolor
2  | attr_2 | sit
3  | attr_3 | amet

Table: Product_Attributes

id | prod_id | attr_id
-------------------------
1  | 1       | 1
2  | 1       | 2
3  | 2       | 1
4  | 2       | 2
5  | 2       | 3

I have generated a result using multiple joins, which looks as follows:

product_name | attribute_name | product_description
---------------------------------------------------
prod_1       | attr_1         | lorem
prod_1       | attr_2         | lorem
prod_2       | attr_1         | ipsum
prod_2       | attr_2         | ipsum
prod_2       | attr_3         | ipsum

The query used for above result is as follows:

function getProductList() {
  return $this->db->select('p.name as product_name, a.name as attribute_name, p.description as product_description')
                  ->from('products as p')
                  ->join('product_attributes as pa', 'pa.prod_id = p.id', 'LEFT')
                  ->join('attributes as a', 'a.id = pa.attr_id', 'LEFT')
                  ->get();
}

But, what I want as a result of the query is as follows:

product_name | attribute_name           | product_description
-------------------------------------------------------------
prod_1       | (attr_1, attr_2)         | lorem
prod_2       | (attr_1, attr_2, attr_3) | ipsum

The drawback of the current query result is that I have to perform a nested loop on the result to display a list of products and their attributes, which affects the performance. I’m open for any suggestion(s) to improve the performance of the query or its result.

–EDIT–

I also have other tables linked with the Products table. Say, for example, there’s an additional table as follows:

Table: Dimensions

id | name   | value
-----------------
1  | length | 20
2  | breadth| 15
3  | height | 20

Table: Product_Dimensions

id | prod_id | dim_id
-------------------------
1  | 1       | 1
2  | 1       | 2
3  | 1       | 3
4  | 2       | 1
5  | 2       | 2

Thus, the expected output modified as follows:

product_name | attribute_name           | product_description| dimension_name            | dimension_value
----------------------------------------------------------------------------------------------------------
prod_1       | (attr_1, attr_2)         | lorem              | (length, breadth, height) | (20, 15, 20)*
prod_2       | (attr_1, attr_2, attr_3) | ipsum              | (length, breadth)         | (20, 15)

But, the obtained output is as follows:

product_name | attribute_name                                   | product_description| dimension_name                                      | dimension_value
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
prod_1       | (attr_1, attr_2, attr_1, attr_2, attr_1, attr_2) | lorem              | (length, breadth, height, length, breadth, height)  | (20, 15, 20, 20, 15, 20)
prod_2       | (attr_1, attr_2, attr_3, attr_1, attr_2, attr_3) | ipsum              | (length, breadth, length, breadth, length, breadth) | (20, 15, 20, 15, 20, 15)

–EDIT–

When used DISTINCT under GROUP_BY, the output gets modified as follows:

product_name | attribute_name           | product_description| dimension_name            | dimension_value
----------------------------------------------------------------------------------------------------------
prod_1       | (attr_1, attr_2)         | lorem              | (length, breadth, height) | (20, 15)*
prod_2       | (attr_1, attr_2, attr_3) | ipsum              | (length, breadth)         | (20, 15)

*You can see the difference between the expected and obtained output. The intended duplicates also get erased by using DISTINCT.

SQL Fiddle to try your hands on here.

5

Answers


  1. you can use the MySQL function GROUP_CONCAT to get the desired result :

    http://sqlfiddle.com/#!9/c51040/3

    Login or Signup to reply.
  2. You will need to do it lile this

    function getProductList() {
      return $this->db->select('p.name as product_name
                         , GROUP_CONCAT(a.name SEPARATOR ",") as attribute_name
                         , min(p.description) as product_description')
                      ->from('products as p')
                      ->join('product_attributes as pa', 'pa.prod_id = p.id', 'LEFT')
                      ->join('attributes as a', 'a.id = pa.attr_id', 'LEFT')
                      ->group_by("p.name");
                      ->get();
    }
    
    Login or Signup to reply.
  3. If you wish to reach your expected value, you have to use GROUP BY. For CodeIgniter you find it here, you find it Guide page in this part $this->db->group_by().

    function getProductList() {
      return $this->db->select('p.name as product_name, concat('( ', GROUP_CONCAT(a.name), ' )') as attribute_name, p.description as product_description')
                      ->from('products as p')
                      ->join('product_attributes as pa', 'pa.prod_id = p.id', 'LEFT')
                      ->join('attributes as a', 'a.id = pa.attr_id', 'LEFT')
                      ->group_by(array('p.name', 'p.description'))
                      ->get();
    }
    

    As you see here, grouping by the columns name i wish to print, and concat the column name you wish to retrieve. Hope this helps, Good luck.

    EDITED

    If you want to use concat or any of aggregation functions on more than one column, you have to do it as GROUP_CONCAT(a.name) as show below.

    function getProductList() {
          return $this->db->select('p.name as product_name, 
                    GROUP_CONCAT(a.name) as attribute_name, 
                    GROUP_CONCAT(a.name) as attribute_name_2, 
                    GROUP_CONCAT(a.name) as attribute_name_3, 
                    p.description as product_description')
                      ->...
                      ->group_by(array('p.name', 'p.description'))
                      ->get();
    }
    

    Be aware, if you want to concat a column, you have not set it with group by statement.

    These examples will give you a good idea about it, Good luck.

    EDITED

    This should work.

    function getProductList() {
          return $this->db->select('p.name as product_name, 
                    GROUP_CONCAT(a.name) as attribute_name, 
                    p.description as product_description, 
                    GROUP_CONCAT(d.name) as dimension_name, 
                    GROUP_CONCAT(d.value) as dimension_value')
                      ->...
                      ->group_by(array('p.name', 'p.description'))
                      ->get();
    }
    

    EDITED

    You could use distinct inside the group_concat so it gives a uniq data as you wish.

    function getProductList() {
          return $this->db->select('p.name as product_name, 
                    GROUP_CONCAT(DISTINCT a.name) as attribute_name, 
                    p.description as product_description, 
                    GROUP_CONCAT(DISTINCT d.name) as dimension_name, 
                    GROUP_CONCAT(DISTINCT d.value) as dimension_value')
                      ->...
                      ->group_by(array('p.name', 'p.description'))
                      ->get();
    }
    

    EDITED

    As i told you in the comments sections, you have to use subquery if you wish to do what you want, And this is how you can do it.

    function getProductList() {
      return $this->db->select('pname, pdesc, aname, adesc, group_concat(dname), group_concat(dvalue)')
                      ->from('( select
                            p.name as pname,
                            p.description as pdesc,
                            GROUP_CONCAT(DISTINCT a.name) as aname,
                            GROUP_CONCAT(DISTINCT a.description) as adesc,
                            d.name as dname, 
                            d.value as dvalue
    
                            from products as p
                            left join product_attributes as pa on pa.product_id = p.id
                            left join attributes as a on a.id = pa.attribute_id
                            left join product_dimensions as pd on pd.product_id = p.id
                            left join dimensions as d on d.id = pd.dimension_id
    
                            group by p.id, p.description, d.name) as d' )
                      ->group_by(array('pname', 'pdesc', 'aname', 'adesc'))
                      ->get();
    }
    

    sql fiddle

    Login or Signup to reply.
  4. I would just execute three simple queries. Get products. Get attributes. Get dimensions.
    Then map attributes and dimensions to corresponding products.
    This is how probably any ORM is performing eager loading.

    I’m not familiar with codeigniter, but I think it should look something like this:

    $query = $this->db->get('products');
    
    $products = [];
    foreach ($query->result() as $row) {
        // init empty arrays for attributes & dimensions
        $row->attributes = [];
        $row->dimensions = [];
        // should be indexed by id, so we can find id later to map attributes & dimensions
        $products[$row->id] = $row;
    }
    
    $attributes = $this->db
        ->select('pa.product_id, a.name')
        ->from('product_attributes as pa')
        ->join('attributes as a', 'a.id = pa.attribute_id')
        ->get();
    
    $dimensions = $this->db
        ->select('pd.product_id, d.name, d.value')
        ->from('product_dimensions as pd')
        ->join('dimensions as d', 'd.id = pd.dimension_id')
        ->get();
    
    foreach ($attributes as $attr) {
        $products[$attr->product_id]->attributes[] = $attr->name;
    }
    
    foreach ($dimensions as $dim) {
        $products[$dim->product_id]->dimensions[] = $dim;
        unset($dim->product_id);
    }
    

    Now you get all your data in a nicely nested structure. With echo json_encode($products, JSON_PRETTY_PRINT) you would get:

    {
        "1": {
            "id": 1,
            "name": "Product_1",
            "description": "Lorem",
            "attributes": [
                "Attribute_1",
                "Attribute_2"
            ],
            "dimensions": [
                {
                    "name": "length",
                    "value": "15"
                },
                {
                    "name": "breadth",
                    "value": "25"
                },
                {
                    "name": "height",
                    "value": "15"
                }
            ]
        },
        "2": {
            "id": 2,
            "name": "Product_2",
            "description": "Ipsum",
            "attributes": [
                "Attribute_1",
                "Attribute_2",
                "Attribute_3"
            ],
            "dimensions": [
                {
                    "name": "length",
                    "value": "15"
                },
                {
                    "name": "breadth",
                    "value": "25"
                }
            ]
        }
    }
    

    You can traverse it in the view layer or your export code, and output in any way you need.

    Update

    In MySQL 5.7+ you can get the same result as a single JSON string with a single query:

    select json_arrayagg(json_object(
      'id', p.id,
      'name', p.name,
      'description', p.description,
      'attributes', (
        select json_arrayagg(a.name)
        from product_attributes pa
        join attributes a on a.id = pa.attribute_id
        where pa.product_id = p.id
      ),
      'dimensions', ( 
        select json_arrayagg(json_object(
          'name',  d.name,
          'value', d.value
        ))
        from product_dimensions pd
        join dimensions d on d.id = pd.dimension_id
        where pd.product_id = p.id
      )
    ))
    from products p;
    

    db-fiddle

    Or maybe you want something “simple” like this:

    select p.*, (
        select group_concat(a.name order by a.id separator ', ')
        from product_attributes pa
        join attributes a on a.id = pa.attribute_id
        where pa.product_id = p.id
      ) as attributes, (
        select group_concat(d.name, ': ', d.value order by d.id separator ', ')
        from product_dimensions pd
        join dimensions d on d.id = pd.dimension_id
        where pd.product_id = p.id
      ) as dimensions
    from products p;
    

    which will return:

    | id  | name      | description | attributes                            | dimensions                          |
    | --- | --------- | ----------- | ------------------------------------- | ----------------------------------- |
    | 1   | Product_1 | Lorem       | Attribute_1, Attribute_2              | length: 15, breadth: 25, height: 15 |
    | 2   | Product_2 | Ipsum       | Attribute_1, Attribute_2, Attribute_3 | length: 15, breadth: 25             |
    

    db-fiddle

    But if you need the result exactly as in the question, then you can try this one:

    select 
      p.name as product_name,
      (
        select concat('(', group_concat(a.name order by a.id separator ', '), ')')
        from product_attributes pa
        join attributes a on a.id = pa.attribute_id
        where pa.product_id = p.id
      ) as attribute_name,
      concat('(', group_concat(d.name order by d.id separator ', '), ')') as dimension_name,
      concat('(', group_concat(d.value order by d.id separator ', '), ')') as dimension_value,
      p.description as product_description
    from products p
    left join product_dimensions pd on pd.product_id = p.id
    left join dimensions d on d.id = pd.dimension_id
    group by p.id;
    

    Result:

    | product_name | product_description | attribute_name                          | dimension_name            | dimension_value |
    | ------------ | ------------------- | --------------------------------------- | ------------------------- | --------------- |
    | Product_1    | Lorem               | (Attribute_1, Attribute_2)              | (length, breadth, height) | (15, 25, 15)    |
    | Product_2    | Ipsum               | (Attribute_1, Attribute_2, Attribute_3) | (length, breadth)         | (15, 25)        |
    

    db-fiddle

    Login or Signup to reply.
  5. Try This using Sub Query.
    Codeigniter Code :

    $this->db->select("select p.name as pname, p.description as pdesc, t2.aname, t2.adesc,t3.dname,t3.dvalue");
    $this->db->from("products p");
    $this->db->join("(select pa.product_id,GROUP_CONCAT(a.name) as aname, GROUP_CONCAT(a.description) as adesc from  product_attributes as pa 
    left join attributes as a on a.id = pa.attribute_id
    group by pa.product_id) t2","t2.product_id=p.id","left");
    $this->db->join("(select group_concat(name) as dname,group_concat(value) as dvalue,pd.product_id from product_dimensions pd
    left join dimensions d on d.id = pd.dimension_id
    group by pd.product_id) t3","t3 on t3.product_id = p.id","left");
    $this->db->group_by("p.id");
    $this->db->get()->result_array();
    

    MySql Query :

    select p.name as pname, p.description as pdesc, t2.aname, t2.adesc,t3.dname,t3.dvalue
    from products as p
    left join (select pa.product_id,GROUP_CONCAT(a.name) as aname, GROUP_CONCAT(a.description) as adesc from  product_attributes as pa 
    left join attributes as a on a.id = pa.attribute_id
    group by pa.product_id) t2 on t2.product_id=p.id
    left join
    (select group_concat(name) as dname,group_concat(value) as dvalue,pd.product_id from product_dimensions pd
    left join dimensions d on d.id = pd.dimension_id
    group by pd.product_id) t3 on t3.product_id = p.id
    group by p.id
    

    Click here to see result

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search