MYSQL get all posts grouped by date - mysql

I want to get all posts in my database and have them returned, grouped by date (year -> month -> day -> posts for this day).
My current database structure:
ID, author, title, content, created_at
And possible values can be:
1, 1, "hello world", "This is the content", "2020-07-27 09:15:57"
2, 1, "Another post", "Another post content", "2020-07-27 11:09:55"
3, 1, "Third post", "No content", "2020-07-28 08:15:20"
...etc
What i want to achieve is, getting all these posts, but group them by date (year, month, day).
i.e.:
[2020] => [
[7] => [
[27] => [
[ID] => 1,
[ID] => 2,
],
[28] => [
[ID] => 3,
]
]
]
What i've tried so far:
SELECT id FROM my_posts_table WHERE author = 1 GROUP BY created_at ORDER BY created_at ASC
AND
SELECT id, YEAR(created_at) as year, MONTH(created_at) as month, DAY(created_at) as day FROM my_posts_table WHERE author = 1 GROUP BY year, month, day ORDER BY year, month, day ASC
My expected result would be the above example of what i want to achieve. But what i'm actually getting is:
[0] => [
[ID] => 1,
[year] => 2020,
[month] => 7,
[day] => 27
],
[1] => [
[ID] => 3,
[year] => 2020,
[month] => 7,
[day] => 28
]
I am completely confused at this moment because i thought 'GROUP BY' was working as i expected, but it appears to be working in a different way.
Can you guys point me in the right direction? I hope it's clear what i'm trying to achieve.
But if it's not, here is it again: "I want to get all the posts from my_posts_table where author_id = 1 and have them returned by year -> month -> day -> posts", So every post on day "27" should be under the [27] key..
EDIT: I made a SQLFiddle as requested

Use GROUP_CONCAT
For your SQLFiddle example try this:
SELECT GROUP_CONCAT(id),
YEAR(created_at) as year, MONTH(created_at) as month, DAY(created_at) as day
FROM my_posts_table
WHERE author = 1
GROUP BY year, month, day ORDER BY year, month, day ASC

A database query always returns a "flat" resultset, made of rows and columns. If you want some a nested datastructure, then one approach is to write a scalar query, that returns just one rows and one column containing a JSON object.
This would involve several levels of aggregation, and JSON functions JSON_ARRAYAGG() and JSON_OBJECT():
select json_object(created_year, json_arrayagg(obj)) obj
from (
select
created_year,
json_object(created_month, json_arrayagg(obj)) obj
from (
select
year(created_at) created_year,
month(created_at) created_month,
json_object(day(created_at), json_arrayagg(id)) obj
from my_post_table
group by year(created_at), month(created_at), day(created_at)
) t
group by created_year, created_month
) t

Related

PostgreSQL approach to producing nested JSON from a flat set of records

Got an intentionally un-normalized table of data:
SELECT location, day, title, teacher, "startTime", "endTime", canceled from "Schedules" ORDER by location, day, "startTime";
Data...
"First", 1, "Knitting 101", "Doe, John", "2019-02-19T09:00Z", "2019-02-19T11:00Z"
"First", 2, "Model Building 201", "Doe, Jane", "2019-02-20T09:00Z", "2019-02-20T11:00Z"
"Second", 1, "Rocks for Jocks 101", "Smith, Terry", "2019-02-19T09:00Z", "2019-02-19T11:00Z"
"Second", 2, "Speed Reading 101", "Case, Justin", "2019-02-20T09:00Z", "2019-02-20T11:00Z"
and want to produce this JSON:
[
{
"location": "First",
"days": [
{
"day": 1,
"classes": [
{
"title": "Knitting 101",
"teacher": "Doe, John",
"starts": "2019-02-19T09:00Z"
"ends": "2019-02-19T11:00Z",
"canceled": false
}
... other classes
]
}
... other days
]
}
... other locations
]
Currently taking results of the query and nesting it using a JavaScript function. Trying to use JSON functions unsuccessfully with this approach:
(SELECT DISTINCT location FROM "Schedules" locations ORDER BY location) l
(SELECT location, day FROM "Schedules" GROUP BY location ORDER BY location, day) d
(SELECT * FROM "Schedules" ORDER BY location, day, "startTime") c
array_to_json(array_agg(row_to_json(l)))
array_to_json(array_agg(row_to_json(d))) sub-query where d.location = l.location
array_to_json(array_agg(row_to_json(c))) sub-query where c.location = l.location and c.day = d.day
Is this the right way to approach it or am I missing a more elegant solution?
UPDATE: Solution provided by #FXD was to start with the most detailed organization level and work backwards. Here is the working query (small adjustments for quoting mixed case objects:
SELECT array_to_json(array_agg("LocationClasses" ORDER BY Location)) AS "FullSchedule"
FROM (
SELECT location, json_build_object('location', location, 'days', array_to_json(array_agg("LocationDayClasses" ORDER BY day))) AS "LocationClasses"
FROM (
SELECT location, day, json_build_object('day', day, 'classes', array_to_json(array_agg(json_build_object('title', title, 'teacher', teacher,'starts', "startTime", 'ends', "endTime", 'cancelled', canceled) ORDER BY "startTime"))) as "LocationDayClasses"
FROM "Schedules"
GROUP BY location, day
) T1
GROUP BY location
) T2
You got the steps right but IMO in reversed order.
It is possible to do everything in 1 query if you build the classes, group them per location/day, then group all days per location and finally group the entire thing together to have 1 record.
From the query below, I advise you try to see what the T1 subquery does, then do the same for T2 subquery. You'll see it wasn't so complicated.
SELECT array_to_json(array_agg(LocationClasses ORDER BY Location)) AS FullSchedule
FROM (
SELECT Location, json_build_object('location',Location, 'days', array_to_json(array_agg(LocationDayClasses ORDER BY day))) AS LocationClasses
FROM (
SELECT Location, day, json_build_object('day', Day, 'classes', array_to_json(array_agg(json_build_object('title',Title,'teacher',Teacher,'starts',Starts,'ends',Ends,'cancelled',false) ORDER BY starts))) as LocationDayClasses
FROM Schedules
GROUP BY Location, Day
) T1
GROUP BY Location
) T2

Get Two matches for each jobs

I have two tables like below
Jobs:
id | user_id | job_title
Matches: id| job_id | user_id
There are one to many relationship between jobs and matches table. One jobs have multiple matches.
I want list of jobs with it's two matches like below :
Jobs => [
'0' => [
'job_title' => 'abc',
'Matches' => [
'0' => [
'id',
'job_id',
'user_id',
],
'0' => [
'id',
'job_id',
'user_id',
]
]
],
'1' => [
'job_title' => 'abc',
'Matches' => [
'0' => [
'id',
'job_id',
'user_id',
],
'0' => [
'id',
'job_id',
'user_id',
]
]
]
]
Please help me on this concern. Thanks in advance.
I have search a lot for this issue and most of the blogs give me same solution like below but it is not work for me.
set #num := 0, #group := '';
select person, `group`, age
from
(
select person, `group`, age,
#num := if(#group = `group`, #num + 1, 1) as row_number,
#group := `group` as dummy
from mytable
order by `Group`, Age desc, person
) as x
where x.row_number <= 2;
You need to extract that data using a join and then to create a variable and save on it the row number(#num).
We also need another variable to save the "group" id (job id in this case) as its necessary to restart the row count when a different job appears.
Finally we filter by #num with as much rows you want. Remember matchNO is a calculated field so you cant filter in a where statment, use having instead.
set #num := 0, #job := 0;
select j.job_title, m.id, m.job_id, m.user_id,IF(#job = j.id,#num:=#num+1,#num:=1) as matchNO,#job:=j.id
from jobs j
left join matches m on j.id = m.job_id
having matchNO <= 2;
The row number is necessary to figure out in what row you are.
The job variable tell you the job in the last row so you can compare and then set #num to 1 again when necesary.
I hope it helps

Query two tables with mysql to count total topics by hour

my forum have two tables to store topics (posts and forums_archive_posts)
$this->DB->build( array(
'select' => "HOUR( FROM_UNIXTIME( post_date ) ) as hour, COUNT(*) AS postCount",
'from' => 'posts',
'where' => "new_topic=0 AND author_id=" . $member['member_id'],
'group' => 'HOUR( FROM_UNIXTIME( post_date ) )',
) );
This query works only with the first table (posts), I need a query that works also in "forum_archive_posts"
tables structure (forum_archive_posts=posts):
archive_author_id = author_id
archive_content_date = post_date
No idea of how you do it in that format but something like
Select postHour, sum(postcount) as postCount From
(
select Hour ... as PostHour
From
Posts...
Union
Select Hour ...
From
ForumArchivePosts ...
) dummyTableName
Group by PostHour
should do the job. the do count bny hoiurs from both tables, then add them together.

MySQL - How to select only the first X rows that have the same field value?

Here is a simple table that describes an event in a calendar:
Event
--------------------
Id int
DayId int # Foreign key to Day table
Title varchar(32)
Start datetime
Finish datetime
And an arbitrary SELECT statement to obtain some result set:
select
Id,
DayId,
Title,
Start,
Finish
from Event
where Start > now()
order by Start
The above select query will return all events in the future, which is undesirable. But using limit means you are required to know how many you want to limit by.
I would like to be able to select the first X rows that have the same DayId values.
Some examples to help explain this situation better:
Example results:
Id: 26, DayId: 08, Title: "Foo", Start: "2012-03-19 23:00:00"
Id: 27, DayId: 08, Title: "Bar", Start: "2012-03-20 00:00:00"
Id: 28, DayId: 09, Title: "Baz", Start: "2012-03-21 09:00:00"
Id: 29, DayId: 10, Title: "Barbaz", Start: "2012-03-22 11:00:00"
Id: 30, DayId: 09, Title: "Fooboo", Start: "2012-03-25 15:00:00"
Assuming the above query returned a result set like this, the query that I'm looking for will only return the first two rows, as they are the next occurring events with the same DayId.
However, after 19th March, the Start > now() condition will return a different result set:
Id: 28, DayId: 09, Title: "Baz", Start: "2012-03-21 09:00:00"
Id: 29, DayId: 10, Title: "Barbaz", Start: "2012-03-22 11:00:00"
Id: 30, DayId: 09, Title: "Fooboo", Start: "2012-03-25 15:00:00"
And in this circumstance, the result should only return the first one row. Note that (for explanatory purposes) the last result does have the same DayId, but because it is separated by a different DayId, it should be ignored.
This might not be the most optimal way to achieve the desired result, but it will work:
select
Id,
DayId,
Title,
Start,
Finish
from Event
where Start > now(), AND DayId IN (SELECT DayId FROM Event WHERE Start > now() ORDER BY Start)
order by Start
Just realized you can't use LIMIT on a subquery, right now this answer won't work
Give this a try:
select id, dayid, title start from (
select id,
#equal := #equal and dayId = #dayId ShouldReturn,
#dayId := dayId as dayId,
title,
start
from t, (
select #dayId := dayid, #equal := true from t
where start = (
select min(start) from t
where start > now()
)) init
where start > now()
order by start
) as final
where ShouldReturn
As I understand it, you want all rows with the "first" dayId. So how about this
SELECT
e1.Id,
e1.DayId,
e1.Title,
e1.Start,
e1.Finish
FROM Event e1.
LEFT JOIN Event e2 ON e2.DayId>e1.DayId AND e2.Start<e1.Start
WHERE e1.DayId = (
SELECT MIN(DayId) FROM Event WHERE Start > now()
)
AND e2.id IS NULL
So, you get the first DayId in all future events in the sub-query and then get all events that have that DayId.
Chevi's answer is almost perfect. It should work, but MySQL has a limitation of not being able to use limit inside a subquery. But a simple workaround is possible:
select
Id,
DayId,
Title,
Start,
Finish
from Event
where Start > now(),
and DayId in (
-- Prepare yourself for a mega-hack!
select * from (
select `DayId`
from Event
where Start > now
order by Start
limit 1
-- You are allowed a limit here for some reason
) alias
)
order by Start
This does work, but a sub-sub query acting as a table alias doesn't seem a very optimal solution, so this answer will not be marked as answered, in case someone can help answer the question better.
SELECT
e.*
FROM
Event AS e
JOIN
( SELECT DayId
FROM Event
WHERE Start > NOW()
ORDER BY Start
LIMIT 1
) AS good
ON e.Start > NOW()
AND e.Start < COALESCE(
( SELECT Start
FROM Event
WHERE Start > NOW()
AND DayId <> good.DayId
ORDER BY Start
LIMIT 1
)
, '9999-12-31')
ORDER BY
e.Start
or:
SELECT
e.*
FROM
Event AS e
CROSS JOIN
( SELECT Start
FROM
Event AS ee
CROSS JOIN
( SELECT DayId
FROM Event
WHERE Start > NOW()
ORDER BY Start
LIMIT 1
) AS good
WHERE Start > NOW()
AND ee.DayId <> good.DayId
ORDER BY Start
LIMIT 1
) AS bad
WHERE
e.Start > NOW()
AND
e.Start < COALESCE(bad.Start, '9999-12-31')
ORDER BY
e.Start

MYSQL, DATETIME, Events, Group by Month/Year in SQL or at output?

I have an events table with a start_date field, a standard date time. I am looking to select all the events in the table, and then output them in groups, separated by the events respective Month and year.
I am using this query:
SELECT Event.id, Event.name, Event.start_date, Event.end_date,
EXTRACT(MONTH FROM `Event`.`start_date`) AS `event_month`,
EXTRACT(YEAR FROM `Event`.`start_date`) AS `event_year`
FROM `my_events` AS `Event`
which produces the following $events array: (using CakePHP)
Array
(
[0] => Array
(
[Event] => Array
(
[id] => 1
[name] => Event Name One
[start_date] => 2011-07-03 11:00:00
[end_date] => 2011-07-03 16:00:00
)
[0] => Array
(
[event_month] => 7 // This event occurs in July
[event_year] => 2011
)
)
[1] => Array
(
[Event] => Array
(
[id] => 2
[name] => Event Name Two
[start_date] => 2011-07-09 11:00:00
[end_date] => 2011-07-09 15:00:00
)
[0] => Array
(
[event_month] => 7 // this event occurs in July
[event_year] => 2011
)
)
[2] => Array
(
[Event] => Array
(
[id] => 3
[name] => Event Name Three
[start_date] => 2011-07-09 11:00:00
[end_date] => 2011-07-09 15:00:00
)
[0] => Array
(
[event_month] => 8 // this event occurs in august.
[event_year] => 2011
)
)
I am looking to now cycle through these arrays, and produce output such as:
July
Event Name One
Event Name Two
August
Event Name Three
I have opted to use the EXTRACT SQL function as it appeared to be quite a common method of grabbing data for such a purpose.
I considered that array_merge might be an option, but $result = array_merge($event) doesn't appear to do anything.
Am I on the right lines, or would there be a better way to achieve this?
Thanks.
use group by:
select month( event.start_date), year( event.start_date), group_concat( distinct event.name ) from my_events event group by year(event.start_date), month(event.start_date);
You can use the YEAR() and MONTH() or the MONTHNAME() functions but I don't think you need GROUP BY but ORDER BY:
SELECT YEAR( Event.start_date ) AS event_year
, MONTH( Event.start_date ) AS event_month
, MONTHNAME( Event.start_date ) AS event_month_name
, Event.id
, Event.name
, Event.start_date,
, Event.end_date
FROM my_events AS Event
WHERE Event.start_date >= '2011-07-01'
AND Event.start_date < '2013-01-01'
ORDER BY Event.start_date ASC
And you use the event_year and event_month in your PHP code to add the July, August, etc... and 2011 tags when the month or year changes from one row to the next.