1

I have 3 tables like below:

users: (id,name)

id name
1 John
2 Arthur

images: (id,user_id,path,createdAt)

id user_id path createdAt
1 1 image_path 2021-07-11 05:38:15
2 1 image_path 2021-07-10 05:38:15
3 1 image_path 2021-07-9 05:38:15
4 1 image_path 2021-07-8 05:38:15

follow: (id,followee,follower)

id followee follower
1 1 2

What I want: A SQL query to get all followee's names, total images, and most recent 3 images(id and path) for a specific follower.

[
   {
        "name": "John",
        "total_images": 1,
        "recent_images": [
            {
                "id": 154,
                 "path": "image_path"
            },
            {
                "id": 125,
                "path": "image_path"
            }
        ]
    }
]

What I have tried:

SELECT users.name

 (SELECT COUNT(DISTINCT images.id)
  FROM images
  WHERE images.user_id = follow.followee
 ) AS "total_images",

 (SELECT images.id , images.path
  FROM images
  WHERE images.user_id = follow.followee
  ORDER BY images.createdAt DESC
  LIMIT 3
 ) AS "recent_images"

FROM follow
LEFT JOIN users ON followers.followee = users.id
WHERE follow.follower = 2

Server type: MySQL ; Server version: 8.0.27

ERROR:

Subquery returns more than 1 row

Rabby hossain
  • 19
  • 1
  • 3
  • Move the subqueries to the FROM clause. – jarlh Oct 28 '21 at 08:35
  • mysql ? sqlserver ? which one ? – Ali Fidanli Oct 28 '21 at 08:38
  • 2
    You cannot use statement like this in inner query which selects 3 records ... also any desired result will help alot – Ali Fidanli Oct 28 '21 at 08:40
  • see: [Can I concatenate multiple MySQL rows into one field?](https://stackoverflow.com/questions/276927/can-i-concatenate-multiple-mysql-rows-into-one-field) – pilchard Oct 28 '21 at 08:50
  • @pilchard Thank you,what I understand from your given reference is that I can concatenate a single column in one column but can I do it for multiple columns? Like images.id and images.path – Rabby hossain Oct 28 '21 at 09:14

2 Answers2

2

The second subquery in SELECT returns more than 1 row; which is invalid in SELECT. I see in you latest edit you've added another column. That's going to give you another different error of:

Error 1241: Operand should contain 1 column(s)

With your current query, you probably can do it like this:

SELECT users.name,

 (SELECT COUNT(DISTINCT images.id)
  FROM images
  WHERE images.user_id = follow.followee
 ) AS "total_images",

 (SELECT GROUP_CONCAT(CONCAT(images.id ,': ', images.path)) AS 'image_and_path'
  FROM images
  WHERE images.user_id = follow.followee
  ORDER BY images.createdAt DESC
  LIMIT 3
 ) AS "recent_images"

FROM follow
LEFT JOIN users ON follow.followee = users.id
WHERE follow.follower = 2;

That will give you results like:

name total_images recent_images
John 4 1: image_path,2: image_path,3: image_path,4: image_path

One row with a concatenated columns + a GROUP_CONCAT() from the second subquery in SELECT.

Otherwise, you can do something like this:

SELECT u1.name AS 'Followee',
       COUNT(*) OVER (PARTITION BY f.follower) Total_images,
       createdAt
FROM users u1 
JOIN images i ON u1.id=i.user_id
JOIN follow f ON u1.id=f.followee
WHERE f.follower=2
ORDER BY createdAt DESC
LIMIT 3;

With this results:

Followee Total_images id path createdAt
John 4 1 image_path 2021-07-11 05:38:15
John 4 2 image_path 2021-07-10 05:38:15
John 4 3 image_path 2021-07-09 05:38:15

Demo fiddle

Update:
To make the value recognizable as JSON array and object, you can add combinations of JSON_OBJECT and JSON_ARRAY. Here's MySQL functions that create JSON values docs:

SELECT JSON_ARRAY(JSON_OBJECT('name',name,'total_images',total_images,'recent_images',
           JSON_ARRAY(GROUP_CONCAT(recent_images))))
FROM
(SELECT u.name ,
        COUNT(*) OVER (PARTITION BY f.follower) AS total_images,
        JSON_OBJECT('id',i.id,'path',i.path) recent_images
FROM follow f
JOIN users u ON f.followee = u.id
JOIN images i ON i.user_id = f.followee
WHERE f.follower = 2
ORDER BY i.createdAt DESC
LIMIT 3) AS V
GROUP BY name, total_images

If LIMIT is not working for you (probably due to sql_mode? I'm not sure), then I'd suggest you use ROW_NUMBER() (or RANK()) function to assign the value with incremental numbering from 1 - XX in descending order. Here's how:

SELECT JSON_ARRAY(JSON_OBJECT('name',name,'total_images',total_images,'recent_images',
           JSON_ARRAY(GROUP_CONCAT(recent_images))))
FROM
(SELECT u.name ,
        COUNT(*) OVER (PARTITION BY f.follower) AS total_images,
        JSON_OBJECT('id',i.id,'path',i.path) recent_images,
        ROW_NUMBER() OVER (PARTITION BY f.follower ORDER BY i.createdAt DESC) rn
FROM follow f
JOIN users u ON f.followee = u.id
JOIN images i ON i.user_id = f.followee
WHERE f.follower = 2) AS V
WHERE rn <= 3
GROUP BY name, total_images;

Updated fiddle

FanoFN
  • 6,815
  • 2
  • 13
  • 33
  • Thank you, your first solution gives me a string field but I want array of objects. Is there any way? Another point is LIMIT is not working. – Rabby hossain Oct 28 '21 at 10:53
  • The part where LIMIT is not working, does it return any error message? About formatting the results as array, I'll get back to you on that. – FanoFN Oct 28 '21 at 13:25
0

I have inserted the records in images as follows:

id user_id path createdAt
1 2 image_path 2021-07-11 05:38:15
2 2 image_path 2021-07-11 05:38:16
3 2 image_path 2021-07-11 05:38:17
4 2 image_path 2021-07-11 05:38:18
5 1 image_path 2021-07-11 05:38:19
6 1 image_path 2021-07-11 05:38:20

So your SQL result should return multiple rows for the recent 3 images of every follower of an individual followee. Here the SQL query with additional columns those you can omit later:

SELECT users.name AS followee, (SELECT COUNT(*) FROM images WHERE images.user_id = follow.followee) AS total_images, images.user_id AS followerId, (SELECT users.name FROM users WHERE id = follow.follower) AS followerName, images.id AS followerImgId, images.path FROM images JOIN follow ON follow.follower = images.user_id JOIN users ON follow.followee = users.id ORDER BY images.createdAt ASC LIMIT 0, 3;
followee total_images followerId followerName followerImgId path
John 2 2 Arthur 4 image_path
John 2 2 Arthur 3 image_path
John 2 2 Arthur 2 image_path

Now, to output the JSON array:

SELECT 
    JSON_ARRAY(JSON_OBJECT('name',name,'total_images',total_images,'recent_images',
    JSON_ARRAY(GROUP_CONCAT(recent_images))))
FROM
    (SELECT 
        users.name, 
        (SELECT COUNT(*) FROM images WHERE images.user_id = follow.followee) AS total_images, 
        JSON_OBJECT ('id',images.id,'path',images.path) AS recent_images 
     FROM images 
     JOIN follow ON follow.follower = images.user_id 
     JOIN users ON follow.followee = users.id 
     ORDER BY images.createdAt ASC LIMIT 0, 3) SQL2JSON;

[{"name": "John", "total_images": 2, "recent_images": ["{\"id\": 4, \"path\": \"image_path\"},{\"id\": 3, \"path\": \"image_path\"},{\"id\": 2, \"path\": \"image_path\"}"]}]

N.B.: You may need to perform few string replacements with the result JSON for removing any unnecessary double-quotes, back slashes etc.