1

I wonder how to replace such a table (the table is the result of 3x LEFT JOIN)

SELECT *
FROM users
LEFT JOIN items on users.id = items.id
LEFT JOIN items_additional on users.id = items_additional.items_id
LEFT JOIN items_ask_user on users.id = items_ask_user.items_id';
ID item_id name surname addition question amount
1 1 Gladys Warner hot-dog mayo 14
2 1 Gladys Warner pizza chilli 11
3 2 Harrison Croft pizza
4 2 Harrison Croft burger chilli 11
5 2 Harrison Croft hod-dog mayo 14

to somthing like

ID item_id name surname addition addition2 addition3 question1 question2 question3 amount
1 1 Gladys Warner hot-dog pizza - mayo chilli - 25
2 2 Harrison Croft pizza burger hod-dog chilli mayo - 25

the number of additions or questions may increase or decrease, depending on person.

Edit

SET @sql = NULL;

WITH cte AS(
    SELECT DISTINCT ROW_NUMBER() OVER(PARTITION BY user_id) AS idx
    FROM users
    LEFT JOIN items on users.id = items.id
    LEFT JOIN items_additional on users.id = items_additional.items_id
    LEFT JOIN items_ask_user on users.id = items_ask_user.items_id
)
SELECT GROUP_CONCAT(
           CONCAT('MAX(IF(rn_add = ', cte.idx, ', additional_option_name, NULL)) AS additional_option_name', cte.idx, ','
                  'MAX(IF(rn_qst = ', cte.idx, ', ask_user, NULL)) AS ask_user', cte.idx
       )) INTO @sql
FROM cte;

SET @cte = 'WITH cte AS(SELECT *, ROW_NUMBER() OVER(PARTITION BY name, surname ORDER BY IF(additional_option_name IS NULL, 1, 0), `event_items`.`id`) AS rn_add, ROW_NUMBER() OVER(PARTITION BY name, surname ORDER BY IF(ask_user IS NULL, 1, 0), `event_items`.`id`) AS rn_qst 
    FROM users
    LEFT JOIN event_items on users.id = event_items.id
    LEFT JOIN event_items_additional on users.id = event_items_additional.items_id
    LEFT JOIN event_items_ask_user on users.id = event_items_ask_user.items_id';

SET @sql = CONCAT(@cte,
                  'SELECT `event_items`.`id`, user_id, name, surname,',
                  @sql,
                  ',SUM(additional_option_price) AS additional_option_price FROM cte GROUP BY user_id, name, surname'
);

SELECT @sql;

PREPARE stmt1 FROM @sql;
EXECUTE stmt1;

DEALLOCATE PREPARE stmt1;

Edit2

Schema demo here

Hub
  • 13
  • 5
  • Do you really need separate (and therefore dynamic) columns for *addition* and *question*? You could just aggregate the values as strings. – Stu Jun 05 '22 at 18:27
  • can you share your coding attempt at this problem? – lemon Jun 05 '22 at 18:31
  • i tried using pivot from SQL Server before i notice that MySQL can't do the same way @lemon – Hub Jun 05 '22 at 19:05
  • @Hub is the schema fixed (max 3 additions/questions), or is it dynamic (unknown number of additions/questions) as supposed by Stu? – lemon Jun 05 '22 at 19:08
  • Yes i can but I am curious if it is possible differently @Stu – Hub Jun 05 '22 at 19:08
  • @lemon it's dynamic – Hub Jun 05 '22 at 19:09
  • A dynamic solution is feasible but more expensive than Stu's smart idea. If you're more interested in the dynamic one, I'll leave an answer below. @Hub – lemon Jun 05 '22 at 19:11
  • Thanks. @lemon I need to export the table to Excel, which makes the dynamic solution seem more flexible in this case. – Hub Jun 05 '22 at 19:18
  • Also thanks @Stu for nice idea :) it will be useful to me many times – Hub Jun 05 '22 at 19:20

2 Answers2

2

Will just throw this here as a possibility - it won't give you dynamic columns but may be of use depending on how you intend to consume the data.

It's certaintly less faff and more performant if you can.

select 
    item_Id, name, surname, 
    group_concat(addition separator ', ') Additions,
    group_concat(question separator ', ') Questions,
    Sum(amount) amount
from t
group by item_Id, name, surname;
Stu
  • 30,392
  • 6
  • 14
  • 33
1

As long as the dynamic solution is based on the static one, for reasons of clarity I'll first explain the static one by assuming that, as in the example your provided, there are exactly 3 fields at max, for addition and question fields.

Let's look at the static solution first, by assuming that we have specifically 3 fields. In this case what you can do is compute a row number for each addition and question, which will be used to match the specific value at the required index for each of the three fields addition1, addition2 and addition3 (same goes for question), using an IF statement. In order to remove the NULL values that are generated by this statement, we can select the maximum value and aggregate over item_id, name and surname

WITH cte AS(
       SELECT *, 
           ROW_NUMBER() OVER(
               PARTITION BY name, surname
               ORDER     BY IF(addition IS NULL, 1, 0), 
                            ID                ) AS rn_add, 
           ROW_NUMBER() OVER(
               PARTITION BY name, surname
               ORDER     BY IF(question IS NULL, 1, 0), 
                            ID                ) AS rn_qst
       FROM tab 
)
SELECT item_id AS ID,
       item_id,
       name,
       surname,
       MAX(IF(rn_add = 1, addition, NULL)) AS addition1,
       MAX(IF(rn_add = 2, addition, NULL)) AS addition2,
       MAX(IF(rn_add = 3, addition, NULL)) AS addition3,
       MAX(IF(rn_qst = 1, question, NULL)) AS question1,
       MAX(IF(rn_qst = 2, question, NULL)) AS question2,
       MAX(IF(rn_qst = 3, question, NULL)) AS question3,
       SUM(amount)                                         AS amount
FROM cte
GROUP BY item_id,
         name,
         surname

Check the demo here.


The dynamic solution aims at reproducing that exact same query as a prepared statement (which is essentially a string that you first build and then ask MySQL to execute over the database), with the only difference that it needs to generalize on the amount of fields to extract:

MAX(IF(rn_add = 1, addition, NULL)) AS addition1,
MAX(IF(rn_qst = 1, addition, NULL)) AS question1,
    ...
    ...
MAX(IF(rn_add = <n>, addition, NULL)) AS addition<n>,
MAX(IF(rn_qst = <n>, addition, NULL)) AS question<n>,

And we need to reproduce these instructions n times with n equals to the item_id's highest amount of both addition and question values. In order to generate this piece of query, we get the longest list of indices:

WITH cte AS(
    SELECT DISTINCT ROW_NUMBER() OVER(PARTITION BY item_id) AS idx
    FROM tab
)

and cycle over it to generate all MAX rows as a string where, in place of the specific number (as in the static query), we will use all the numbers stored inside cte.idx:

SELECT GROUP_CONCAT(
           CONCAT('MAX(IF(rn_add = ', cte.idx, ', addition, NULL)) AS addition', cte.idx, ','
                  'MAX(IF(rn_qst = ', cte.idx, ', question, NULL)) AS question', cte.idx
       )) INTO @sql
FROM cte;

Once we have the generalized amonut of MAX rows, we can just use this together with the rest of the static query, which does not depend on the number of addition or question values.

SET @cte = 'WITH cte AS(SELECT *, ROW_NUMBER() OVER(PARTITION BY name, surname ORDER BY IF(addition IS NULL, 1, 0), ID) AS rn_add, ROW_NUMBER() OVER(PARTITION BY name, surname ORDER BY IF(question IS NULL, 1, 0), ID) AS rn_qst FROM tab)';

SET @sql = CONCAT(@cte,
                  'SELECT item_id AS ID, item_id, name, surname,',
                  @sql,
                  ',SUM(amount) AS amount FROM cte GROUP BY item_id, name, surname'
);

Once we have the static query generated as a string in a dynamic way, we can ask MySQL to prepare, execute and deallocate it.

PREPARE stmt1 FROM @sql;
EXECUTE stmt1;

DEALLOCATE PREPARE stmt1;

The execution will show you your desired output.

Here's the full code for the dynamic query:

SET @sql = NULL;

WITH cte AS(
    SELECT DISTINCT ROW_NUMBER() OVER(PARTITION BY item_id) AS idx
    FROM tab
)
SELECT GROUP_CONCAT(
           CONCAT('MAX(IF(rn_add = ', cte.idx, ', addition, NULL)) AS addition', cte.idx, ','
                  'MAX(IF(rn_qst = ', cte.idx, ', question, NULL)) AS question', cte.idx
       )) INTO @sql
FROM cte;

SET @cte = 'WITH cte AS(SELECT *, ROW_NUMBER() OVER(PARTITION BY name, surname ORDER BY IF(addition IS NULL, 1, 0), ID) AS rn_add, ROW_NUMBER() OVER(PARTITION BY name, surname ORDER BY IF(question IS NULL, 1, 0), ID) AS rn_qst FROM tab)';

SET @sql = CONCAT(@cte,
                  'SELECT item_id AS ID, item_id, name, surname,',
                  @sql,
                  ',SUM(amount) AS amount FROM cte GROUP BY item_id, name, surname'
);

SELECT @sql;

PREPARE stmt1 FROM @sql;
EXECUTE stmt1;

DEALLOCATE PREPARE stmt1;

Check the demo here.


Side Note: if you want to store the output of this query, you may require to create a view inside the prepared statement. In that case you should change the @sql assignment to:

SET @sql = CONCAT('CREATE VIEW my_view AS ',
                  @cte,
                  'SELECT item_id AS ID, item_id, name, surname,',
                  @sql,
                  ',SUM(amount) AS amount FROM cte GROUP BY item_id, name, surname'
);

hence select the content of the view whenever you need it, for example to export it to Excel.

lemon
  • 14,875
  • 6
  • 18
  • 38
  • Thanks for very good answer @lemon :) I update my question after a few attempts to write this query. Not sure whats wrong – Hub Jun 06 '22 at 09:07
  • If you can catch the differences between the sql demo published here and your data, I may help you further @Hub – lemon Jun 06 '22 at 10:08
  • did it solve your problem then? – lemon Jun 06 '22 at 10:21
  • the difference is adding left joins instead of tab table @lemon – Hub Jun 06 '22 at 10:21
  • there is a mistake somewhere in my query, i get message "Duplicate column name 'id'. " @lemon I stucked here – Hub Jun 06 '22 at 10:25
  • 1
    yes, needs a moment – Hub Jun 06 '22 at 10:26
  • Typically that error is due when you join two tables and each of the two tables have the same column field, then it does not know where to pick that column from https://stackoverflow.com/questions/4815627/1060-duplicate-column-name-id. Can you create a fiddle for debugging at https://www.db-fiddle.com/? @Hub – lemon Jun 06 '22 at 10:27
  • 1
    i put fiddle for debugging demo in edit2 of my post @lemon. Sorry for so long, but I had to edit my tables – Hub Jun 06 '22 at 11:13
  • Do you have a query that gets you successfully from the fiddle tables to the input table published in this post? @Hub – lemon Jun 06 '22 at 13:27
  • yes, query similar to Stu answer. I seved it in fiddle – Hub Jun 06 '22 at 13:49
  • https://www.db-fiddle.com/f/iZ1nYthiEMsJxKcUgUrGSp/6 can you tell me if this is the static output you need? (once we get the static one working, the dynamic is easy) @Hub – lemon Jun 06 '22 at 14:05
  • why there's the same user_id for different combination of (name, surname)? @Hub this detail conflicts with the creation of the final table – lemon Jun 06 '22 at 15:23
  • oh, thats why in fiddle doesn't works. Thanks to you, I was able to create something like this https://www.db-fiddle.com/f/iZ1nYthiEMsJxKcUgUrGSp/8 In db it works but in fiddle no, propably because of Views? @lemon user_id is diffrent by my mistake – Hub Jun 06 '22 at 15:28
  • If you ask about table `event_items` ? @lemon – Hub Jun 06 '22 at 15:32
  • In the first generated table I've changed `event_items.user_id` to `users.id AS user_id`. Here's a reference to the updated static version https://www.db-fiddle.com/f/iZ1nYthiEMsJxKcUgUrGSp/10 and to the dynamic one https://www.db-fiddle.com/f/iZ1nYthiEMsJxKcUgUrGSp/9. Does it solve your problem? @Hub – lemon Jun 06 '22 at 16:06
  • 1
    Great! I understand very well now :) It solvs the problem! @lemon Thanks – Hub Jun 06 '22 at 16:14
  • How you can call this type of query? Its posible to write it without concatenate (in one big query)? @lemon – Hub Jun 06 '22 at 16:30
  • As far as I know, this is not possible. Dynamic query generation requires multiple steps, namely 1. the query to cycle over the variable (`cte.idx`), 2. the query to build the string (`SET @sql = ....`), 3. the transformation from the string to the prepared statement (`PREPARE @sql`), 4. the execution of the prepared statement (`EXECUTE @sql`), 5. the deallocation of the prepared statement (`DEALLOCATE @sql`). @Hub – lemon Jun 06 '22 at 16:35
  • If you want to "*call*" this set of instructions, you need to put them inside a stored procedure. Check here: https://dev.mysql.com/doc/refman/8.0/en/create-procedure.html @Hub – lemon Jun 06 '22 at 16:39