1

I have a table:

CREATE TABLE `Issues` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

I have another table:

CREATE TABLE `Attachments` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `issue_id` int(11) DEFAULT NULL,
  `attachment` text,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

How can I get the data to look like this:

issue_id    title    attachment1    attachment2    attachment3
--------------------------------------------------------------
1           T1       a1.png         a2.png
2           T2
3           T3       b4.gif         xyz.doc        ttt.file

The problem I can't figure out is how to get the dynamic set of attachments into a dynamic column grouped by issue. I have determined that the maximum amount of attachments for one issue is 12, but the total per ticket can be anywhere from 0-12. I'm stumped...

I've tried this MySQL pivot row into dynamic number of columns, but can't make sense of it in my case because I'm building dynamic columns based on total matches per record...

Any help would be greatly appreciate. Please let me know if this doesn't make sense.

Nino

Community
  • 1
  • 1
Nino Skilj
  • 281
  • 3
  • 6

2 Answers2

2

The set of columns returned by a SELECT statement are statically defined. A SELECT statement can't return a "variable" number of columns.

It is possible to produce the resultset you show, if you can define the columns to be returned, which in your case, means defining a maximum number of attachment values (columns) that will be returned on a row.

One approach to getting that resultset is to use a correlated subquery in the SELECT list to return the first, second, third, etc. occurrence of attachment values.

SELECT i.id
     , i.title
     , ( SELECT a1.attachment
           FROM `Attachments` a1
          WHERE a1.issue_id = i.id
          ORDER BY a1.id
          LIMIT 0,1
        ) AS attachment1
     , ( SELECT a2.attachment
           FROM `Attachments` a2
          WHERE a2.issue_id = i.id
          ORDER BY a2.id
          LIMIT 1,1
        ) AS attachment2
     , ( SELECT a3.attachment
           FROM `Attachments` a3
          WHERE a3.issue_id = i.id
          ORDER BY a3.id
          LIMIT 2,1
        ) AS attachment3
  FROM `Issues` i
 ORDER BY i.id

To return your maximum number of attachment, you would need to extend that...

     , ( SELECT a4.attachment
           FROM `Attachments` a4
          WHERE a4.issue_id = i.id
          ORDER BY a4.id
          LIMIT 3,1
        ) AS attachment4

The purpose of the ORDER BY is to make the resultset from the query deterministic (absent the ORDER BY, MySQL can return rows in whatever order it wants.)

The purpose of the LIMIT clause is to specify that only 1 row be returned. LIMIT 0,1 specifies that 1 row is to be returned, starting with the first row (0). LIMIT 1,1 returns only the second row.


This is not the only approach, and may not be the most efficient. It can work reasonably for a small number of rows returned from the outer query (in your case, from the Issues table. The "nested loops" plan that is generated for this statement can be resource intensive (i.e. slow) for a large set.

For best performance, you'll likely want an index...

ON `Attachments` (issue_id, id) 

or at least on

ON `Attachments` (issue_id)

If you truly need a "dynamic" number returned, you're going to be better served returning the attachment values as separate rows, and processing the resultset returned from the SQL statement on the client side.

spencer7593
  • 106,611
  • 15
  • 112
  • 140
2

Here is one way to accomplish this if you know the max number is 12. It uses MAX and CASE, getting the Row Number for each attachment.

SELECT 
    I.Id issue_id, 
    I.title, 
    MAX(CASE WHEN d.row_number = 1 THEN D.attachment END) attachment1,
    MAX(CASE WHEN d.row_number = 2 THEN D.attachment END) attachment2,
    MAX(CASE WHEN d.row_number = 3 THEN D.attachment END) attachment3,
    MAX(CASE WHEN d.row_number = 4 THEN D.attachment END) attachment4,
    MAX(CASE WHEN d.row_number = 5 THEN D.attachment END) attachment5,
    MAX(CASE WHEN d.row_number = 6 THEN D.attachment END) attachment6,
    MAX(CASE WHEN d.row_number = 7 THEN D.attachment END) attachment7,
    MAX(CASE WHEN d.row_number = 8 THEN D.attachment END) attachment8,
    MAX(CASE WHEN d.row_number = 9 THEN D.attachment END) attachment9,
    MAX(CASE WHEN d.row_number = 10 THEN D.attachment END) attachment10,
    MAX(CASE WHEN d.row_number = 11 THEN D.attachment END) attachment11,
    MAX(CASE WHEN d.row_number = 12 THEN D.attachment END) attachment12
FROM Issues I
  LEFT JOIN (
  SELECT 
      a.issue_id, 
      @running:=if(@previous=a.issue_id,@running,0) + 1 as row_number,
      @previous:=a.issue_id,
      a.attachment
  FROM Attachments a
      JOIN    (SELECT @previous := 0) r
    ORDER BY a.issue_id, a.attachment
  ) D ON I.ID = D.issue_id
GROUP BY I.Id, I.Title

And here is the SQL Fiddle.

I had to make an edit to make the rownumber reset per group. Should be working now. Also, per @spencer7593's great comment, I've updated the query slightly.

--EDIT--

In response to OP's comment about needing dynamic results, this should work:

SET @sql = NULL;

SELECT
  GROUP_CONCAT(DISTINCT
    CONCAT(
      'MAX(IF(d.row_number = ', d.row_number, ',D.attachment,NULL)) AS attachment', d.row_number)
  ) INTO @sql
FROM Issues I
  LEFT JOIN (
  SELECT 
      a.issue_id, 
      @running:=if(@previous=a.issue_id,@running,0) + 1 as row_number,
      @previous:=a.issue_id,
      a.attachment
  FROM Attachments a
      JOIN    (SELECT @previous := 0) r
    ORDER BY a.issue_id, a.attachment
  ) D ON I.ID = D.issue_id
;

SET @sql = CONCAT('SELECT I.Id issue_id, 
                          I.title, ', @sql, ' 
                  FROM Issues I
                  LEFT JOIN (
                  SELECT 
                      a.issue_id, 
                      @running:=if(@previous=a.issue_id,@running,0) + 1 as row_number,
                      @previous:=a.issue_id,
                      a.attachment
                  FROM Attachments a
                      JOIN    (SELECT @previous := 0) r
                    ORDER BY a.issue_id, a.attachment
                  ) D ON I.ID = D.issue_id
                GROUP BY I.Id, I.Title');

PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

And here is the SQL Fiddle.

sgeddes
  • 62,311
  • 6
  • 61
  • 83
  • 1
    +1. It would be good to initialize `@previous := NULL` in the inline view aliased as `r`, to insulate the statement from any previous value stored in the variable @previous. In the inline view aliased as `d`, the first a.issue_id in the SELECT list is redundant; the column alias `issue_id` could be assigned on the line that sets @previous:=a.issue_id. Also, an ORDER BY in that inline view would make the statement more deterministic, in that the attachments would be returned in a specified sequence, rather than allowing MySQL to return them in an arbitrary order. – spencer7593 Feb 09 '13 at 01:55
  • @spencer7593 -- wonderful comments, many thanks! I left curRow mistakenly from a previous version :) I've edited my reply -- thanks again! – sgeddes Feb 09 '13 at 02:05
  • @sgeddes -- Thanks! This is great and meets my immediate need. Next I'll have to figure out how to receive the same result by using the max count of attachments grouped by id (that's how I got 12 to begin with). So when the maximum potential attachments for one id changes, the columns would dynamically change as well. If you know how to do that, it would completely solve my use case! – Nino Skilj Feb 15 '13 at 00:56
  • @NinoSkilj -- you'll need to use Dynamic SQL to accomplish this -- I edited my answer above -- glad I could help! – sgeddes Feb 15 '13 at 02:23