5

For easier explaination I will try to simplify everything.

I have 3 SQL tables: Users, Certs, Serv

In Users table are stored details about unique users.

In Certs table are stored details about certificates and UserId of user who have this certificate (1 user can have multiple certificates)

In Serv table are stored details about sea services and UserId of user (like Certs table, 1 user can have multiple entries in Serv table)

SAMPLE DATA

Users

UserId  Name
1       John
2       Lisa

Certs

Id  UserId  CertName
1   1       A
2   1       B
3   1       C
4   2       A
5   2       C

Serv

UserId  Name
1       SA
1       SB
1       SC
1       SD
2       S2A

I need to retrieve output via PHP something like (where UserId = 1) also for reality there will be more columns from each table (for example more details of certificates like date of issue, date of expire, place of issue and so on):

Personal details:
Name
John

Certificates:
Certificate Id    Certificate Name
1                 A 
2                 B
3                 C

Sea Services:
Sea Service Name
SA
SB
SC
SD

But I got wrong output, duplicated entries, that because joining 2 tables with UserId in whose are multiple records with this UserId.

PHP CODE

$users = $con->prepare("
    select u.Name 
           ,GROUP_CONCAT(c.Id SEPARATOR '<br>') AS Id
           ,GROUP_CONCAT(c.certsName SEPARATOR '<br>') AS certsName 
           ,GROUP_CONCAT(s.Name SEPARATOR '<br>') AS Name         
    from users u
    left join certs c on u.UserId = c.UserId
    left join serv s on u.UserId = s.UserId 
    where u.UserId = ?
");
$users->bind_param('i', $GetUserId);
$users->execute();

$users->bind_result(
    $userName,
    $certId,            
    $certName,          
    $servName
);

<?php 
while ($users->fetch()) {
?>

<span>Personal Details</span>
<div class="grid-group">
    <div class="grid-column">
        <div class="grid-item header">User Name </div> 
    </div>
    <div class="grid-column">
        <div class="grid-item"><?php echo $userName; ?></div>       
    </div>
</div>

<span>Certificates</span>
<div class="grid-group">
    <div class="grid-column">
        <div class="grid-item header">Certificate Id</div> 
        <div class="grid-item header">Certificate Name</div> 
    </div>
    <div class="grid-column">
        <div class="grid-item"><?php echo $certId; ?></div> 
        <div class="grid-item"><?php echo $certName; ?></div>       
    </div>
</div>

<span>Sea Services</span>
<div class="grid-group">
    <div class="grid-column">
        <div class="grid-item header">Sea Service Name</div> 
    </div>
    <div class="grid-column">
        <div class="grid-item"><?php echo $servName; ?></div>       
    </div>
</div>

<?php } ?>

You can check SQL FIDDLE to see what results selecting, duplicating rows.

Have you ideas how can I achieve desired output without duplicates?

UPDATE

After using GROUP_CONCAT with DISTINCT It's still wrong. Imagine that In Serv table I have columns like: UserId, Name, Rank and Country

If the same user worked in different companies (this Name in example - Company Name) with the same rank in different countries, It selecting wrong data. For example:

Serv table (SQL)

UserId  Name     Rank        Country
1       SA       Captain     USA
1       SB       Captain     USA
1       SC       Captain     RUS
1       SD       Captain     ENG
2       S2A      Engineer    USA 

If I use query like this:

select u.Name 
       ,GROUP_CONCAT(distinct c.Id SEPARATOR '<br>') AS Id
       ,GROUP_CONCAT(distinct c.certsName SEPARATOR '<br>') AS certsName 
       ,GROUP_CONCAT(distinct s.Name SEPARATOR '<br>') AS Name  
       ,GROUP_CONCAT(distinct s.Rank SEPARATOR '<br>') AS Rank
       ,GROUP_CONCAT(distinct s.Country SEPARATOR '<br>') AS Country        
from users u
left join certs c on u.UserId = c.UserId
left join serv s on u.UserId = s.UserId 
where u.UserId = ?

So GROUP_CONCAT(DISTINCT..) returning me in following:

......
Sea Services:
Sea Service Name    Rank      Country
SA                  Captain   USA
SB                            RUS
SC                            ENG
SD                        

Only first row have rank and first 3 rows have countries returned, but in database are stored ranks and countries for every row.

Full desired output with this data should be like this:

Personal details:
Name
John

Certificates:
Certificate Id    Certificate Name
1                 A 
2                 B
3                 C

Sea Services:
Sea Service Name    Rank      Country
SA                  Captain   USA
SB                  Captain   USA             
SC                  Captain   RUS
SD                  Captain   ENG

You can check It at SQL FIDDLE

UPDATE 2

If I remove DISTINCT I got following output:

Sea Service Name    Rank        Country
SA                  Captain     USA
SA                  Captain     USA
SA                  Captain     USA
SB                  Captain     USA
SB                  Captain     USA
SB                  Captain     USA
SC                  Captain     RUS
SC                  Captain     RUS
SC                  Captain     RUS
SD                  Captain     ENG
SD                  Captain     ENG
SD                  Captain     ENG

If I'm using DISTINCT I got like this:

Sea Services:
Sea Service Name    Rank      Country
SA                  Captain   USA
SB                            RUS
SC                            ENG
SD                        

But It should be:

Sea Services:
Sea Service Name    Rank      Country
SA                  Captain   USA
SB                  Captain   USA             
SC                  Captain   RUS
SD                  Captain   ENG

UPDATE 3

Imagine that I have fixed width of columns and I have long Sea Service Name which will be wrapped to new row:

Sea Service Name |  Rank  | Country
-----------------|--------|---------
This is long Sea | Captain| USA
Service Name     |--------|---------
-----------------| Captain| RUS
 Other Name      |--------|---------
-----------------| Captain| ENG
Another long Sea |--------|---------
Service Name     | Master | USA                        
-----------------|--------|---------
Other Sea Serv   |
-----------------|

As you see now each column are separate, rows didn't match. But It should be like 1 row. So I think I can't achieve It with GROUP_CONCAT, looks like I need another way.

How It looks in real:

rows not match

Infinity
  • 828
  • 4
  • 15
  • 41
  • A piece of advice: don't do formatting of data in SQL or in any data access layer. Use it to extract data, then use application logic to transform that data for presentation, or for other use cases. – Unix One Apr 14 '16 at 06:09

6 Answers6

5

You are missing a group by clause :

select u.Name 
       ,GROUP_CONCAT(distinct c.Id SEPARATOR '<br>') AS Id
       ,GROUP_CONCAT(distinct c.certsName SEPARATOR '<br>') AS certsName 
       ,GROUP_CONCAT(distinct s.Name SEPARATOR '<br>') AS Name  
       ,(SELECT GROUP_CONCAT(ss.Rank SEPARATOR '<br>') FROM users uu
         LEFT OUTER JOIN serv ss ON (uu.UserId = ss.UserId)
         WHERE uu.user_id = u.user_id) as Rank
       ,GROUP_CONCAT(distinct s.Country SEPARATOR '<br>') AS Country        
from users u
left join certs c on u.UserId = c.UserId
left join serv s on u.UserId = s.UserId 
where u.UserId = ?

Also, I've added distinct to your GROUP_CONCAT since you are left joining to multiple tables with multiples rows for each user, you will have multiple duplicates.

sagi
  • 40,026
  • 6
  • 59
  • 84
  • Thank you so much! `distinct` was what I needed! – Infinity Apr 05 '16 at 07:45
  • I don't understand how you got this output from this query, they don't even match on columns.. show me the exact output for me to understand. – sagi Apr 05 '16 at 10:47
  • This was only sample query, for reality there are more than 100 columns. Updated my question with sample query how I use It (added 2 more columns `Rank` and `Country`). Also provided desired output for this sample. – Infinity Apr 05 '16 at 10:52
  • Updated my question with more data, hope It's clearly now. If not - ask me, I will try to explain once again. – Infinity Apr 05 '16 at 11:08
  • Updated my question with SQL FIDDLE, you can check It here: http://sqlfiddle.com/#!9/1ab01/1 For testing purpose try to remove `DISTINCT` from Rank and you will see, Rank will be duplicated 12 times, instead of 4. It should be 4 rows, that because in `Serv` table are 4 rows for this `UserId` – Infinity Apr 05 '16 at 11:25
  • The output looks fine, it works perfectly. And its ok that there is 12, think about it, your a joining between multiple tables on only USERID = USERID, so, in first table you have 1 record, in the second table you have 3 records, for only this two joins you will have 1X3 records, and then you join to a table with 4 records for this userid, so now you will have 1X3X4 records , which is 12. Nothing wrong here @Infinity . Joining between tables can and usually will multiply the result set, as long as at least 2 tables have more then 1 record for the join condition. – sagi Apr 05 '16 at 11:32
  • Yea, I'm fully understand It, so I'm looking for any way to avoid this, that because If I'm using `GROUP_CONCAT` without `DISTINCT` It returns me messed up / repeated data instead of correct rows. Maybe there I don't need to use `GROUP_CONCAT`, maybe can I use another query to select data from `SeaService` table? I just don't know how to correctly use 2 queries with loop. Or have you ideas how can I show rows from `SeaServices` table correctly? You can look at my desired output in updated question to see what I need. Hope you understood. – Infinity Apr 05 '16 at 11:45
  • I don't understand what's the problem with the current query? so what if its multiplying the records? It outputs the same thing as if the records wasn't multiplyed, thats what the distinct is for! @Infinity – sagi Apr 05 '16 at 11:52
  • Imagine that `John` has job on `SA` company and his rank was `Captain` and he worked at `USA`. Next year `John` found new job `SB` he also worked with the same rank `Captain` and company also was in `USA`. And next year he found new job again `SC`, also rank was `Captain`, but company was in `RUS`. If I'm using `DISTINCT` It returns me all 3 companies: `SA`, `SB`, and `SC`, but only 1 rank for `SA` company, that because in all 3 companies he was `Captain` and only 2 countries: `USA` and `RUS` for `SA` and `SB` companies what is also incorrect. It missing data in this case. – Infinity Apr 05 '16 at 11:59
  • Please show me on fiddle an example where it produce wrong output. – sagi Apr 05 '16 at 12:02
  • Ah, now I understand what you want.. you want something like a half distinct right? Well, unfortunately I need to go, but I'll take a look later on today or tomorrow. @Infinity – sagi Apr 05 '16 at 14:25
  • 1
    Thank you for your help, I'm still waiting for your suggestions. – Infinity Apr 07 '16 at 05:46
  • 1
    Thank you for your help, seems to be working, but I don't think so that It's best solution to use `GROUP_CONCAT`. Check **UPDATE 3** at my question. – Infinity Apr 07 '16 at 08:14
  • Look at sample data which I provided in update 3. If `Sea Service` have long name it wraps and take 2 rows, where `Rank` and `Country` are short and takes only 1 row, so table messes up. As in sample data: `Other Sea Serv` - `Master` - `USA` should be in 1 row. – Infinity Apr 07 '16 at 08:39
  • @infinity then use two rows space between them – sagi Apr 07 '16 at 08:40
  • How can I catch It? Sometime can be country name long (take 2 rows) and company name short (take 1 row). – Infinity Apr 07 '16 at 08:41
  • Always concat this two rows, what does it matter if its one or two – sagi Apr 07 '16 at 08:56
  • I've added screenshot how It looks for real, as you see each column have separate row height. – Infinity Apr 07 '16 at 10:29
  • @Infinity Only name can be two rows? or other columns two? I suggest: play with `CASE EXPRESSION` a bit to decide on each one if to use two
    or 1 -> `CASE WHEN LENGTH(t.Name) > ?? THEN

    else
    end`
    – sagi Apr 07 '16 at 10:40
  • There for real are 10 columns and all can be 2-3 rows expect dates. – Infinity Apr 07 '16 at 11:03
  • Also this is incorrect syntax I think... `SELECT GROUP_CONCAT(ss.Name SEPARATOR (SELECT CASE WHEN LENGTH(ss.Name) > 20 THEN '



    ' ELSE '

    ' END)) FROM....` I don't know how to use case after `SEPARATOR` Have you ideas?
    – Infinity Apr 07 '16 at 11:54
  • No need for `SELECT CASE ...` Only `CASE...` – sagi Apr 07 '16 at 11:55
  • I've tried in both ways with `SELECT` and without `SELECT`, but still syntax error... – Infinity Apr 07 '16 at 11:57
  • Maybe its not possible like this.. What happen if you just put 2 rows for everyone? names/id/rank.... – sagi Apr 07 '16 at 12:11
  • If I pass 2 `

    ` for every column It still messed up. Where should be 1 row now It takes 2 rows, but where should be 2 rows It takes 3 rows (after 2 rows additional `
    `)...
    – Infinity Apr 07 '16 at 12:20
  • 1
    @Infinity: My advice: Don't use one query, use three. The certificates have nothing to do with the sea services, so don't try to join them somehow and then muddle through this data mush in PHP. Select the user data, build the HTML; select the certificates, build the HTML; select the sea services, build the HTML. It can't be easier than that. – Thorsten Kettner Apr 08 '16 at 10:51
3

This looks like a mad amount of effort to avoid multiple queries..

Keep it simple

You could run the query:

SELECT ...        
FROM users u
LEFT JOIN certs c on u.UserId = c.UserId
LEFT JOIN serv s on u.UserId = s.UserId 
WHERE u.UserId = ?

And separate the certificates and services in the application logic.

Or just run two or three separate queries:

    SELECT * FROM users u WHERE u.UserId = ?
    SELECT * FROM certs c WHERE c.UserId = ?
    SELECT * FROM serv s  WHERE s.UserId = ?

Although there is the overhead of 3 queries, with the right indexing these will be crazy fast and you've reduced the amount of redundant data changing hands.

These simple queries are easily debugged and understood. The query you have is very complex for this operation and even minor changes are already causing you issues.

Also, please please please separate out your formatting from the database. If I was having trouble with spacing in a layout, one of the LAST places I'd look is the database query.

Keeping the separate layers of your application separate allows you to deal with one problem at a time and change the display of your data without worrying about the data itself.

Arth
  • 12,789
  • 5
  • 37
  • 69
  • 1
    Thank you for your answer, but I have problems to run 2 queries, maybe I got php issues or I don't know, I have asked here question how to run 2 separate queries, but still no luck to solve It. http://stackoverflow.com/questions/36427041/how-to-execute-2-or-more-sql-queries-in-php-without-joining-tables – Infinity Apr 07 '16 at 10:19
  • Hmm, first sort out your error reporting so you can debug on your own, then work out how to run two queries in a request (looks like you are executing both queries through the same connection before fetching the results of the first). Don't limit your options because of simple bugs. Have you considered using a framework? Lots of this stuff is done for you already. – Arth Apr 07 '16 at 10:42
  • 1
    This is the correct answer. Three different things to select = three separate queries. To create a HTML table or the like from the data read should not be a problem at all. @Infinity: In my opinion you should accept Arth's answer. If you have a problem to run several queries, one after the other, in PHP, then this is another issue which has nothing to do with this request here. – Thorsten Kettner Apr 08 '16 at 10:43
  • I too believe this is the correct answer and the right way to solve this problem – sagi Apr 09 '16 at 19:30
2

This can be simply done using application logic after getting entire data with duplicated entries and separating them in PHP. Always remember you can have multiple application servers to distribute its load but you would always prefer to have single Database server on which you would want minimum load of queries. Also Customer load can be distributed among application servers, where as same number of clients would be accessing same database.

skilledpeas
  • 101
  • 1
  • 7
  • Thank you for an answer, but I couldn't achieve It successfully via PHP, that's why I'm asking how. I don't know how to solve problems with duplicates.Could you provide some code samples? – Infinity Apr 14 '16 at 07:41
0

You needed to add DISTINCT to GROUP_CONCAT because you get join n rows from certs to m rows from serv, resulting in n*m rows with lots of duplicates.

But when you do your GROUP_CONCAT for each table using Derived Tables you get only one row per table:

select u.Name 
       ,c.Id
       ,c.certsName 
       ,s.Name  
       ,s.Rank
       ,s.Country        
from users u
left join
 ( select UserId
       ,GROUP_CONCAT(Id SEPARATOR '<br>') AS Id
       ,GROUP_CONCAT(certsName SEPARATOR '<br>') AS certsName 
   from certs
   where UserId = 1
   group by UserId
 ) as c on u.UserId = c.UserId
left join 
 ( select UserId
       ,GROUP_CONCAT(Name SEPARATOR '<br>') AS Name  
       ,GROUP_CONCAT(Rank SEPARATOR '<br>') AS Rank
       ,GROUP_CONCAT(Country SEPARATOR '<br>') AS Country        
   from serv
   where UserId = 1
   group by UserId
 ) as s on u.UserId = s.UserId 
where u.UserId = 1

See Fiddle

Of course the advice to submit seperate queries is still the best answer, everything else is just a workaround solution.

dnoeth
  • 59,503
  • 4
  • 39
  • 56
  • I already gave him an answer that does that, the problem is the lines alignment when one of the rank/name/certs.. is more then 1 row length.. – sagi Apr 12 '16 at 13:57
  • @sagi: It's a single row per table due to aggregating before the join, if he can't display that in a proper way he'll probably fail with three Selects, too :- – dnoeth Apr 12 '16 at 16:45
  • I agree, I don't think this issue need to concern him, he need to go on another way. Sounds like the xy problem.. – sagi Apr 12 '16 at 16:48
0
SELECT 
u.name,
(SELECT GROUP_CONCAT(c.id SEPARATOR '<br>') FROM certs c WHERE c.userId = u.userId) AS Id,
(SELECT GROUP_CONCAT(c.CertsName SEPARATOR '<br>') FROM certs c WHERE c.userId = u.userId) AS certsName,
(SELECT GROUP_CONCAT(s.Name SEPARATOR '<br>') FROM serv s WHERE s.userId = u.userId) AS Name
FROM users u WHERE u.userId = ?
Wajih
  • 4,227
  • 2
  • 25
  • 40
0

SELECT MULTIPLE ROWS FROM TABLE1(TB1) AND TABLE2(TB2) : CM1_1 , CM1_2 FROM TABLE1 AND CM2_1 , CM2_2 , CM2_3 FROM TABLE2

SELECT CM1_1 , CM1_2 , CM2_1 , CM2_2 , CM2_3 FROM TB1, TB2 WHERE  CM1_2 = '1' AND CM1_1 = CM2_1  AND CM2_3 = 'John'