I have a booking table where all the service booking list where booking details saved like this:
id user_id booking_date booking_id
1 3 2017-01-10 booking1
2 3 2017-01-11 booking1
3 3 2017-01-12 booking1
4 3 2017-01-13 booking1
5 3 2017-01-14 booking1
6 4 2017-01-19 booking2
7 4 2017-01-20 booking2
8 4 2017-01-21 booking2
9 4 2017-01-22 booking2
10 3 2017-02-14 booking3
11 3 2017-02-15 booking3
I want to get a start and end date of booking that came in a row.
like for user_id 3 has 2 date range of booking date
from `2017-01-10 to 2017-01-14`
and then after some records
from `2017-02-14 to 2017-02-15`
First of all, I don't think that getting sequences like that does make sense. ... But, ok.
To do this in one Query would be compicated with that data. So I would first add some column like "group_id" or "order_id". So you can save one ID to all orders that belong together.
Just iterate over the Table, ascending by ID and check if the next (or last) data has the same user_id.
When you do have the order_id column, you can simple
SELECT MIN(booking_date), MAX(booking_date) FROM table GROUP BY order_id
Ok, nobody says it is easy ... let's go. This is a gap and island problem. let me say it is mooooore easy to solve in postges sql
I apply mysql variables to your scenario.
I solve it on SQL Fiddle:
MySQL 5.6 Schema Setup:
create table t ( user_id int, booking_date date );
insert into t values
( 3, '2017-01-10'),
( 3, '2017-01-11'),
( 3, '2017-01-12'),
( 3, '2017-01-13'),
( 3, '2017-01-14'),
( 4, '2017-01-19'),
( 4, '2017-01-20'),
( 4, '2017-01-21'),
( 4, '2017-01-22'),
( 3, '2017-02-14'),
( 3, '2017-02-15');
Query 1:
select user_id, min(booking_date), max(booking_date)
from (
select t1.user_id,
t1.booking_date,
#g := case when(
DATE_ADD(#previous_date, INTERVAL 1 DAY) <> t1.booking_date or
#previous_user <> t1.user_id )
then t1.booking_date
else #g
end as g,
#previous_user:= t1.user_id,
#previous_date:= t1.booking_date
from t t1, ( select
#previous_user := -1,
#previous_date := STR_TO_DATE('01/01/2000', '%m/%d/%Y'),
#g:=STR_TO_DATE('01/01/2000', '%m/%d/%Y') ) x
order by user_id, booking_date
) X
group by user_id, g
Results:
| user_id | min(booking_date) | max(booking_date) |
|---------|-------------------|-------------------|
| 3 | 2017-01-10 | 2017-01-14 |
| 3 | 2017-02-14 | 2017-02-15 |
| 4 | 2017-01-19 | 2017-01-22 |
Explanation nested query figure up a group code ( g ) for each range. The external query get the max and the min for each group.
Related
I've got a table that looks something like the following
Date
Key
Metric
2021-01-31
A
6
2021-02-28
A
3
2021-05-31
A
3
2021-03-31
B
4
2021-04-30
B
1
2021-05-31
B
2
What I'd like to do is insert a row with a metric of 0 for Key A for the date of 2021-03-31, since Key A had already appeared in January in February.
Key B, on the other hand, would ideally stay untouched since it has metrics associated with every date after its appearance. (The table I'm working with happens to be monthly, but I'm sure I could make the changes to make a daily solution work here)
So, Ideally we'd end up with a table looking like the following
Date
Key
Metric
2021-01-31
A
6
2021-02-28
A
3
2021-03-31
A
0
2021-04-30
A
0
2021-05-31
A
3
2021-03-31
B
4
2021-04-30
B
1
2021-05-31
B
2
That's all for now, thank you very much everyone
Fiddle for MySQL 8.0+
There are various ways to do this. The following assumes MySQL 8.0 or better, but less convenient solutions exist for versions prior to this.
The following assumes (xkey, xdate) is unique or the primary key of our table.
CTE term
Description
cte1
Determine the range of dates in our full sequence
cte2
Generate the full list of last dates in each month of the range
cte3
Generate a list of (xkey, MIN(xdate)) pairs
Final
Now generate every potential new row to insert
The INSERT IGNORE only inserts rows which do not already exist, based on the primary key. If a constraint violation would occur, IGNORE will skip that row.
INSERT IGNORE INTO test
WITH RECURSIVE cte1 (min_date, max_date) AS (
SELECT MIN(xdate) AS min_date
, MAX(xdate) AS max_date
FROM test
)
, cte2 (xdate, max_date) AS (
SELECT min_date, max_date FROM cte1
UNION ALL
SELECT LAST_DAY(xdate + INTERVAL 5 DAY), max_date
FROM cte2 WHERE xdate < max_date
)
, cte3 (xkey, xdate) AS (
SELECT xkey, MIN(xdate) FROM test GROUP BY xkey
)
SELECT cte2.xdate, xkey, 0 FROM cte2 JOIN cte3 ON cte3.xdate < cte2.xdate
;
SELECT * FROM test ORDER BY xkey, xdate;
+------------+------+--------+
| xdate | xkey | metric |
+------------+------+--------+
| 2021-01-31 | A | 6 |
| 2021-02-28 | A | 3 |
| 2021-03-31 | A | 0 |
| 2021-04-30 | A | 0 |
| 2021-05-31 | A | 3 |
| 2021-03-31 | B | 4 |
| 2021-04-30 | B | 1 |
| 2021-05-31 | B | 2 |
+------------+------+--------+
The setup:
CREATE TABLE test ( xdate date, xkey varchar(10), metric int, primary key (xdate, xkey) );
INSERT INTO test VALUES
( '2021-01-31', 'A', 6 )
, ( '2021-02-28', 'A', 3 )
, ( '2021-05-31', 'A', 3 )
, ( '2021-03-31', 'B', 4 )
, ( '2021-04-30', 'B', 1 )
, ( '2021-05-31', 'B', 2 )
;
I don't have a profound SQL background, recently encountered a problem with SQL that seems hard to do with JUST SQL.
I have a table
```
IMEI | DATE | A_1 | A_2 | A_3 | B_1 | B_2 | B_3
2132 | 09/21| 2 | 4 | 4 | 5 | 2 | 4
4535 | 09/22| 2 | 2 | 4 | 5 | 2 | 3
9023 | 09/21| 2 | 1 | 5 | 7 | 2 | 2
```
How can I group value of A_1, A_2 etc in a way so I can achieve the this table. Basically, I would like to group certain columns in my table, and put them into different rows.
IMEI | DATE | MODULE | val_1 | val_2 | val_3
2132 | 09/21| A | 2 | 4 | 4
2132 | 09/21| B | 5 | 2 | 4
...
The goal is to have value under namespace A, B etc for a row to be separated into different rows in the new Table.
Also, any suggestions on where can I improve my SQL. any books I should keep as reference or any other resources I should use?
Thanks!
You can do it with UNION:
SELECT IMEI, DATE, 'A' AS MODULE, A_1 AS val_1, A_2 AS val_2, A_3 AS val_3
FROM myTable
UNION ALL
SELECT IMEI, DATE, 'B', B_1, B_2, B_3
FROM myTable
See it on sqlfiddle.
But really, you should store your data in the form created by the above query, and then use a JOIN to create the original format as/when desired.
I love playing with data and questions like this!
Below can be considered as over-engineering but I think it is still an option when you don't know your columns names in advance but have a pattern you described or it can be useful just for learning as looks like you are looking for improving your SQL (based on tag for this question I assume you meant BigQuery SQL)
#standardSQL
WITH parsed AS (
SELECT IMEI, DATE,
REGEXP_REPLACE(SPLIT(row, ':')[OFFSET(0)], r'^"|"$', '') key,
REGEXP_REPLACE(SPLIT(row, ':')[OFFSET(1)], r'^"|"$', '') value
FROM `yourTable` t,
UNNEST(SPLIT(REGEXP_REPLACE(to_json_string(t), r'[{}]', ''))) row
),
grouped AS (
SELECT
IMEI, DATE,
REGEXP_EXTRACT(key, r'(.*)_') MODULE,
ARRAY_AGG(value ORDER BY CAST(REGEXP_EXTRACT(key, r'_(.*)') AS INT64)) AS vals
FROM parsed
WHERE key NOT IN ('IMEI', 'DATE')
GROUP BY IMEI, DATE, MODULE
)
SELECT IMEI, DATE, MODULE,
vals[SAFE_OFFSET(0)] AS val_1,
vals[SAFE_OFFSET(1)] AS val_2,
vals[SAFE_OFFSET(2)] AS val_3,
vals[SAFE_OFFSET(3)] AS val_4
FROM grouped
-- ORDER BY IMEI, DATE, MODULE
You can test / play with dummy data from your question
#standardSQL
WITH `yourTable` AS (
SELECT 2132 IMEI, '09/21' DATE, 2 A_1, 4 A_2, 4 A_3, 5 B_1, 2 B_2, 4 B_3 UNION ALL
SELECT 4535, '09/22', 2, 2 ,4, 5, 2, 3 UNION ALL
SELECT 9023, '09/21', 2, 1 ,5, 7, 2, 2
),
parsed AS (
SELECT IMEI, DATE,
REGEXP_REPLACE(SPLIT(row, ':')[OFFSET(0)], r'^"|"$', '') key,
REGEXP_REPLACE(SPLIT(row, ':')[OFFSET(1)], r'^"|"$', '') value
FROM `yourTable` t,
UNNEST(SPLIT(REGEXP_REPLACE(to_json_string(t), r'[{}]', ''))) row
),
grouped AS (
SELECT
IMEI, DATE,
REGEXP_EXTRACT(key, r'(.*)_') MODULE,
ARRAY_AGG(value ORDER BY CAST(REGEXP_EXTRACT(key, r'_(.*)') AS INT64)) AS vals
FROM parsed
WHERE key NOT IN ('IMEI', 'DATE')
GROUP BY IMEI, DATE, MODULE
)
SELECT IMEI, DATE, MODULE,
vals[SAFE_OFFSET(0)] AS val_1,
vals[SAFE_OFFSET(1)] AS val_2,
vals[SAFE_OFFSET(2)] AS val_3,
vals[SAFE_OFFSET(3)] AS val_4
FROM grouped
ORDER BY IMEI, DATE, MODULE
Output will be as below
Row IMEI DATE MODULE val_1 val_2 val_3 val_4
1 2132 09/21 A 2 4 4 null
2 2132 09/21 B 5 2 4 null
3 4535 09/22 A 2 2 4 null
4 4535 09/22 B 5 2 3 null
5 9023 09/21 A 2 1 5 null
6 9023 09/21 B 7 2 2 null
I am getting number of visits every day for generating a chart. Even when there are zero records, I want to get the record with count 0.
I am planning to create a table which will contain every day, and when fetching - data will join with this table and get count of the records from visit table. Is there any other way to do the same in mySQL?
Visit Table with Sample Data
Date | ........
----------------------
01/11/2014 | --------
03/11/2014 | --------
I want results even for 02/11/2014 with count 0. If I group by date - I will get count only when records exists on a particular date.
I'll try to read in between lines of your question... Sort of game where I write the question and the answer :-/
You have a table (my_stats) holding two fields, one is the date (my_date) the other is a integer (my_counter).
By some mean, you will need a table holding a list of all dates you want to use in your output.
This could be done with a temp table... (but not all hosting solution will allow you this) the other is to build it up on the fly, using a view or a stored procedure.
Then you will LEFT JOIN this table/view/stored procedure/etc... to your table my_visits based on the date field.
This will output you all dates, and when there won't be a match in mour my_visits you'll have a NULL value. ( IFNULL(my_visits.my_counter, 0) will give you a 0 (zero) when there is no matching value.
inspiration:
Get a list of dates between two dates +
How to get list of dates between two dates in mysql select query and a nice solution here that needs no loops, procedures, or temp tables generate days from date range
Based on that last link, here we go...
first a sample table
DROP TABLE IF EXISTS `my_stats`;
CREATE TABLE IF NOT EXISTS `my_stats` (
`my_date` date NOT NULL,
`my_counter` int(11) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
INSERT INTO `my_stats` (`my_date`, `my_counter`) VALUES
('2017-11-01', 2),
('2017-11-02', 3),
('2017-11-03', 5),
('2017-11-05', 3),
('2017-11-07', 7);
And now a working exemple BETWEEN '2017-11-01' AND '2017-11-09'
SELECT date_range.date AS the_date,
IFNULL(my_stats.my_counter, 0) AS the_counter
FROM (
SELECT a.date
FROM (
SELECT Curdate() - INTERVAL (a.a + (10 * b.a) + (100 * c.a)) day
AS date
FROM (
SELECT 0 AS a
UNION ALL
SELECT 1
UNION ALL
SELECT 2
UNION ALL
SELECT 3
UNION ALL
SELECT 4
UNION ALL
SELECT 5
UNION ALL
SELECT 6
UNION ALL
SELECT 7
UNION ALL
SELECT 8
UNION ALL
SELECT 9
) AS a
CROSS JOIN (
SELECT 0 AS a
UNION ALL
SELECT 1
UNION ALL
SELECT 2
UNION ALL
SELECT 3
UNION ALL
SELECT 4
UNION ALL
SELECT 5
UNION ALL
SELECT 6
UNION ALL
SELECT 7
UNION ALL
SELECT 8
UNION ALL
SELECT 9
) AS b
CROSS JOIN (
SELECT 0 AS a
UNION ALL
SELECT 1
UNION ALL
SELECT 2
UNION ALL
SELECT 3
UNION ALL
SELECT 4
UNION ALL
SELECT 5
UNION ALL
SELECT 6
UNION ALL
SELECT 7
UNION ALL
SELECT 8
UNION ALL
SELECT 9
) AS c
) AS a
WHERE a.date BETWEEN '2017-11-01' AND '2017-11-09'
) AS date_range
LEFT JOIN my_stats
ON( date_range.date = my_stats.my_date )
ORDER BY the_date ASC
Output
+------------+-------------+
| the_date | the_counter |
+------------+-------------+
| 2017-11-01 | 2 |
| 2017-11-02 | 3 |
| 2017-11-03 | 5 |
| 2017-11-04 | 0 |
| 2017-11-05 | 3 |
| 2017-11-06 | 0 |
| 2017-11-07 | 7 |
| 2017-11-08 | 0 |
| 2017-11-09 | 0 |
+------------+-------------+
I have a table like : session is the name of the table for example
With columns: Id, sessionDate, user_id
What i need:
Delta should be a new calculated column
Id | sessionDate | user_id | Delta in days
------------------------------------------------------
1 | 2011-02-20 00:00:00 | 2 | NULL
2 | 2011-03-21 00:00:00 | 2 | NULL
3 | 2011-04-22 00:00:00 | 2 | NULL
4 | 2011-02-20 00:00:00 | 4 | NULL
5 | 2011-03-21 00:00:00 | 4 | NULL
6 | 2011-04-22 00:00:00 | 4 | NULL
Delta is the Difference between the timestamps
What i want is a result for Delta Timestamp (in Days) for the the previous row and the current row grouped by the user_id.
this should be the result:
Id | sessionDate | user_id | Delta in Days
------------------------------------------------------
1 | 2011-02-20 00:00:00 | 2 | NULL
2 | 2011-02-21 00:00:00 | 2 | 1
3 | 2011-02-22 00:00:00 | 2 | 1
4 | 2011-02-20 00:00:00 | 4 | NULL
5 | 2011-02-23 00:00:00 | 4 | 3
6 | 2011-02-25 00:00:00 | 4 | 2
I already have a solution for a specific user_id:
SELECT user_id, sessionDate,
abs(DATEDIFF((SELECT MAX(sessionDate) FROM session WHERE sessionDate < t.sessionDate and user_id = 1), sessionDate)) as Delta_in_days
FROM session AS t
WHERE t.user_id = 1 order by sessionDate asc
But for more user_ids i didnĀ“t find any solution
Hope somebody can help me.
Try this:
drop table a;
create table a( id integer not null primary key, d datetime, user_id integer );
insert into a values (1,now() + interval 0 day, 1 );
insert into a values (2,now() + interval 1 day, 1 );
insert into a values (3,now() + interval 2 day, 1 );
insert into a values (4,now() + interval 0 day, 2 );
insert into a values (5,now() + interval 1 day, 2 );
insert into a values (6,now() + interval 2 day, 2 );
select t1.user_id, t1.d, t2.d, datediff(t2.d,t1.d)
from a t1, a t2
where t1.user_id=t2.user_id
and t2.d = (select min(d) from a t3 where t1.user_id=t3.user_id and t3.d > t1.d)
Which means: join your table to itself on user_ids and adjacent datetime entries and compute the difference.
If id is really sequential (as in your sample data), the following should be quite efficient:
select t.id, t.sessionDate, t.user_id, datediff(t2.sessiondate, t.sessiondate)
from table t left outer join
table tprev
on t.user_id = tprev.user_id and
t.id = tprev.id + 1;
There is also another efficient method using variables. Something like this should work:
select t.id, t.sessionDate, t.user_id, datediff(prevsessiondate, sessiondate)
from (select t.*,
if(#user_id = user_id, #prev, NULL) as prevsessiondate,
#prev := sessiondate,
#user_id := user_id
from table t cross join
(select #user_id := 0, #prev := 0) vars
order by user_id, id
) t;
(There is a small issue with these queries where the variables in the select clause may not be evaluated in the order we expect them to. This is possible to fix, but it complicates the query and this will usually work.)
Although you have choosen an answer here is another way of achieving it
SELECT
t1.Id,
t1.sessionDate,
t1.user_id,
TIMESTAMPDIFF(DAY,t2.sessionDate,t1.sessionDate) as delta
from myTable t1
left join myTable t2
on t1.user_id = t2.user_id
AND t2.Id = (
select max(Id) from myTable t3
where t1.Id > t3.Id AND t1.user_id = t3.user_id
);
DEMO
I have a database table which holds each user's checkins in cities. I need to know how many days a user has been in a city, and then, how many visits a user has made to a city (a visit consists of consecutive days spent in a city).
So, consider I have the following table (simplified, containing only the DATETIMEs - same user and city):
datetime
-------------------
2011-06-30 12:11:46
2011-07-01 13:16:34
2011-07-01 15:22:45
2011-07-01 22:35:00
2011-07-02 13:45:12
2011-08-01 00:11:45
2011-08-05 17:14:34
2011-08-05 18:11:46
2011-08-06 20:22:12
The number of days this user has been to this city would be 6 (30.06, 01.07, 02.07, 01.08, 05.08, 06.08).
I thought of doing this using SELECT COUNT(id) FROM table GROUP BY DATE(datetime)
Then, for the number of visits this user has made to this city, the query should return 3 (30.06-02.07, 01.08, 05.08-06.08).
The problem is that I have no idea how shall I build this query.
Any help would be highly appreciated!
You can find the first day of each visit by finding checkins where there was no checkin the day before.
select count(distinct date(start_of_visit.datetime))
from checkin start_of_visit
left join checkin previous_day
on start_of_visit.user = previous_day.user
and start_of_visit.city = previous_day.city
and date(start_of_visit.datetime) - interval 1 day = date(previous_day.datetime)
where previous_day.id is null
There are several important parts to this query.
First, each checkin is joined to any checkin from the previous day. But since it's an outer join, if there was no checkin the previous day the right side of the join will have NULL results. The WHERE filtering happens after the join, so it keeps only those checkins from the left side where there are none from the right side. LEFT OUTER JOIN/WHERE IS NULL is really handy for finding where things aren't.
Then it counts distinct checkin dates to make sure it doesn't double-count if the user checked in multiple times on the first day of the visit. (I actually added that part on edit, when I spotted the possible error.)
Edit: I just re-read your proposed query for the first question. Your query would get you the number of checkins on a given date, instead of a count of dates. I think you want something like this instead:
select count(distinct date(datetime))
from checkin
where user='some user' and city='some city'
Try to apply this code to your task -
CREATE TABLE visits(
user_id INT(11) NOT NULL,
dt DATETIME DEFAULT NULL
);
INSERT INTO visits VALUES
(1, '2011-06-30 12:11:46'),
(1, '2011-07-01 13:16:34'),
(1, '2011-07-01 15:22:45'),
(1, '2011-07-01 22:35:00'),
(1, '2011-07-02 13:45:12'),
(1, '2011-08-01 00:11:45'),
(1, '2011-08-05 17:14:34'),
(1, '2011-08-05 18:11:46'),
(1, '2011-08-06 20:22:12'),
(2, '2011-08-30 16:13:34'),
(2, '2011-08-31 16:13:41');
SET #i = 0;
SET #last_dt = NULL;
SET #last_user = NULL;
SELECT v.user_id,
COUNT(DISTINCT(DATE(dt))) number_of_days,
MAX(days) number_of_visits
FROM
(SELECT user_id, dt
#i := IF(#last_user IS NULL OR #last_user <> user_id, 1, IF(#last_dt IS NULL OR (DATE(dt) - INTERVAL 1 DAY) > DATE(#last_dt), #i + 1, #i)) AS days,
#last_dt := DATE(dt),
#last_user := user_id
FROM
visits
ORDER BY
user_id, dt
) v
GROUP BY
v.user_id;
----------------
Output:
+---------+----------------+------------------+
| user_id | number_of_days | number_of_visits |
+---------+----------------+------------------+
| 1 | 6 | 3 |
| 2 | 2 | 1 |
+---------+----------------+------------------+
Explanation:
To understand how it works let's check the subquery, here it is.
SET #i = 0;
SET #last_dt = NULL;
SET #last_user = NULL;
SELECT user_id, dt,
#i := IF(#last_user IS NULL OR #last_user <> user_id, 1, IF(#last_dt IS NULL OR (DATE(dt) - INTERVAL 1 DAY) > DATE(#last_dt), #i + 1, #i)) AS
days,
#last_dt := DATE(dt) lt,
#last_user := user_id lu
FROM
visits
ORDER BY
user_id, dt;
As you see the query returns all rows and performs ranking for the number of visits. This is known ranking method based on variables, note that rows are ordered by user and date fields. This query calculates user visits, and outputs next data set where days column provides rank for the number of visits -
+---------+---------------------+------+------------+----+
| user_id | dt | days | lt | lu |
+---------+---------------------+------+------------+----+
| 1 | 2011-06-30 12:11:46 | 1 | 2011-06-30 | 1 |
| 1 | 2011-07-01 13:16:34 | 1 | 2011-07-01 | 1 |
| 1 | 2011-07-01 15:22:45 | 1 | 2011-07-01 | 1 |
| 1 | 2011-07-01 22:35:00 | 1 | 2011-07-01 | 1 |
| 1 | 2011-07-02 13:45:12 | 1 | 2011-07-02 | 1 |
| 1 | 2011-08-01 00:11:45 | 2 | 2011-08-01 | 1 |
| 1 | 2011-08-05 17:14:34 | 3 | 2011-08-05 | 1 |
| 1 | 2011-08-05 18:11:46 | 3 | 2011-08-05 | 1 |
| 1 | 2011-08-06 20:22:12 | 3 | 2011-08-06 | 1 |
| 2 | 2011-08-30 16:13:34 | 1 | 2011-08-30 | 2 |
| 2 | 2011-08-31 16:13:41 | 1 | 2011-08-31 | 2 |
+---------+---------------------+------+------------+----+
Then we group this data set by user and use aggregate functions:
'COUNT(DISTINCT(DATE(dt)))' - counts the number of days
'MAX(days)' - the number of visits, it is a maximum value for the days field from our subquery.
That is all;)
As data sample provided by Devart, the inner "PreQuery" works with sql variables. By defaulting the #LUser to a -1 (probable non-existent user ID), the IF() test checks for any difference between last user and current. As soon as a new user, it gets a value of 1... Additionally, if the last date is more than 1 day from the new date of check-in, it gets a value of 1. Then, the subsequent columns reset the #LUser and #LDate to the value of the incoming record just tested against for the next cycle. Then, the outer query just sums them up and counts them for the final correct results per the Devart data set of
User ID Distinct Visits Total Days
1 3 9
2 1 2
select PreQuery.User_ID,
sum( PreQuery.NextVisit ) as DistinctVisits,
count(*) as TotalDays
from
( select v.user_id,
if( #LUser <> v.User_ID OR #LDate < ( date( v.dt ) - Interval 1 day ), 1, 0 ) as NextVisit,
#LUser := v.user_id,
#LDate := date( v.dt )
from
Visits v,
( select #LUser := -1, #LDate := date(now()) ) AtVars
order by
v.user_id,
v.dt ) PreQuery
group by
PreQuery.User_ID
for a first sub-task:
select count(*)
from (
select TO_DAYS(p.d)
from p
group by TO_DAYS(p.d)
) t
I think you should consider changing database structure. You could add table visits and visit_id into your checkins table. Each time you want to register new checkin you check if there is any checkin a day back. If yes then you add a new checkin with visit_id from yesterday's checkin. If not then you add new visit to visits and new checkin with new visit_id.
Then you could get you data in one query with something like that:
SELECT COUNT(id) AS number_of_days, COUNT(DISTINCT visit_id) number_of_visits FROM checkin GROUP BY user, city
It's not very optimal but still better than doing anything with current structure and it will work. Also if results can be separate queries it will work very fast.
But of course drawbacks are you will need to change database structure, do some more scripting and convert current data to new structure (i.e. you will need to add visit_id to current data).