Query BETWEEN not working if it has the same date - php

I have an Oracle query that works fine using 'WHERE something BETWEEN FROM and TO' and if from/to are not the same, but when they are, it doesn't works.
I tried to validate on PHP if from/to are the same then i change the query replacing the BETWEEN for a 'WHERE something = date' but it doesn't works either.
This is the query with the PHP validation i mentioned:
<?php
$db = "(DESCRIPTION=(ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = 191.238.210.61)(PORT = 1521)))(CONNECT_DATA=(SID=opera)))";
$conn = oci_connect("opera", "opera", $db);
if (!$conn):
#--- Si no hay conexión, muestra error y detiene la depuración
$m = oci_error();
echo $m['message'];
elseif ($desde == $hasta):
$consulta = "
SELECT
CONFIRMATION_NO,
NATIONALITY_DESC,
GUEST_NAME,
ADULTS,
CHILDREN,
RATE_CODE,
TRACE_ON,
DEPT_ID,
SUM(ADULTS) OVER()
|| ' ADULTOS Y '
|| SUM(CHILDREN) OVER()
|| ' NIÑOS' AS RESULT
FROM
( SELECT
MAX(CONFIRMATION_NO) AS CONFIRMATION_NO,
NATIONALITY_DESC,
GUEST_FIRST_NAME
|| ' '
|| GUEST_NAME GUEST_NAME,
MAX(ADULTS) AS ADULTS,
MAX(CHILDREN) AS CHILDREN,
RATE_CODE,
MIN(TRACE_ON) AS TRACE_ON,
LISTAGG(GUEST_RSV_TRACES.DEPT_ID, ',') WITHIN GROUP (ORDER BY NULL) AS DEPT_ID
FROM
GUEST_RSV_TRACES,
NAME_RESERVATION
WHERE trace_on = '$desde'
AND guest_rsv_traces.resv_name_id = name_reservation.resv_name_id
AND rate_code like 'ALL%'
AND resv_status not in ('CANCELLED')
AND guest_rsv_traces.dept_id not in ('TRF','OD')
GROUP BY
NATIONALITY_DESC,
GUEST_FIRST_NAME,
GUEST_NAME,
RATE_CODE
)
";
$query = oci_parse($conn, $consulta);
oci_execute($query);
elseif ($desde != $hasta):
$consulta = "
SELECT
CONFIRMATION_NO,
NATIONALITY_DESC,
GUEST_NAME,
ADULTS,
CHILDREN,
RATE_CODE,
TRACE_ON,
DEPT_ID,
SUM(ADULTS) OVER()
|| ' ADULTOS Y '
|| SUM(CHILDREN) OVER()
|| ' NIÑOS' AS RESULT
FROM
( SELECT
MAX(CONFIRMATION_NO) AS CONFIRMATION_NO,
NATIONALITY_DESC,
GUEST_FIRST_NAME
|| ' '
|| GUEST_NAME GUEST_NAME,
MAX(ADULTS) AS ADULTS,
MAX(CHILDREN) AS CHILDREN,
RATE_CODE,
MIN(TRACE_ON) AS TRACE_ON,
LISTAGG(GUEST_RSV_TRACES.DEPT_ID, ',') WITHIN GROUP (ORDER BY NULL) AS DEPT_ID
FROM
GUEST_RSV_TRACES,
NAME_RESERVATION
WHERE trace_on BETWEEN '$desde' and '$hasta'
AND guest_rsv_traces.resv_name_id = name_reservation.resv_name_id
AND rate_code like 'ALL%'
AND resv_status not in ('CANCELLED')
AND guest_rsv_traces.dept_id not in ('TRF','OD')
GROUP BY
NATIONALITY_DESC,
GUEST_FIRST_NAME,
GUEST_NAME,
RATE_CODE
)
";
$query = oci_parse($conn, $consulta);
oci_execute($query);
endif;
?>
Output example: (Using BETWEEN '05-SEP-19' and '10-SEP-19'):
145580 EE.UU JAMES LANTZKE 3 0 ALLIN5NMT 05/09/19 HK 6 ADULTOS Y 0 NIÑOS
167410 EE.UU KATHARINE BLOOD 3 0 ALLIN5NMT 05/09/19 HK 6 ADULTOS Y 0 NIÑOS
SUMMARY:
If i do an (BETWEEN '05-SEP-19' and '10-SEP-19') it works
If i do an (BETWEEN '05-SEP-19' and '05-SEP-19') doesn't works
If i do an (trace_on = '05-SEP-19') doesn't works
I would like to know how i can query the same day using BETWEEN or a way to get results searching an specific date.
¡Thanks in advance!

Dates always have the components: year, month, day, hour, minute and second. User interfaces do not always show all these components (but they are still always there).
If your TRACE_ON column has the date 2019-09-05T12:34:56 and you are trying to see if it is BETWEEN '05-SEP-19' AND '05-SEP-19' then Oracle will (assuming that matches the NLS_DATE_FORMAT session parameter) convert the string to a date at midnight and 2019-09-05T12:34:56 BETWEEN 2019-09-05T00:00:00 AND 2019-09-05T00:00:00 is false so the row will not be returned.
Change your query to:
SELECT
-- ...
TO_CHAR( TRACE_ON, 'YYYY-MM-DD HH24:MI:SS' ),
-- ...
FROM (
SELECT
-- ...
FROM GUEST_RSV_TRACES
INNER JOIN NAME_RESERVATION
ON ( guest_rsv_traces.resv_name_id = name_reservation.resv_name_id )
WHERE trace_on >= TO_DATE( '$desde', 'DD-MON-RR' )
AND trace_on < TO_DATE( '$hasta', 'DD-MON-RR' ) + INTERVAL '1' DAY
AND -- ...
GROUP BY
-- ...
)

If column which is to be compared to those "dates" is of a DATE datatype, you shouldn't compare it to strings, because '05-SEP-19' (as well as all other values) are strings, not dates. Don't rely on implicit conversion - use either to_date function or date literal.
For example:
between date '2019-09-05' and date '2019-09-05'
or (presuming your NLS settings is set to English)
between to_date('05-sep-19', 'dd-mon-rr') and to_date('05-sep-19', 'dd-mon-rr')
Because, if you do it correctly, then between works as expected. Have a look:
SQL> create table test (id, datum) as
2 select 1, date '2019-09-04' from dual union all
3 select 2, date '2019-09-05' from dual union all --> this ...
4 select 3, date '2019-09-05' from dual union all --> ... and this are between 05.09.2019 and 05.09.2019
5 select 4, date '2019-09-06' from dual;
Table created.
SQL> alter session set nls_date_Format = 'dd.mm.yyyy';
Session altered.
SQL> select * From test
2 where datum between date '2019-09-05' and date '2019-09-05';
ID DATUM
---------- ----------
2 05.09.2019
3 05.09.2019
SQL>

Related

Left join same table with count specific value

I have one table where I would like to count the amount of names grouped by school, count whether school has one or two genders as well as count the specific occurence of a particular value in 3 different columns. I am able to do the counts in two different tables but I want to join them to make 1 table
FName
School
Gender
Events
Events2
Events3
Ann
Marymount
F
HJ
LJ
TJ
Peter
Marymount
M
100
200
400
Drew
St Hughs
M
100
200
Davis
St Hughs
M
200
Kat
Campion
F
400
Molly
Campion
F
400
Mike
Marymount
M
800
Fran
Campion
M
100
200
These are the 2 separate queries I use successfully and create two different result sets
$sql = "SELECT COUNT(FName) as cnt, COUNT(DISTINCT(Gender)) as gnd, school, COUNT(*) FROM entries WHERE school <> 'Unattached' GROUP BY school";
$sql = "SELECT COUNT(Events + Events2 + Events3) as events, school, COUNT(*) FROM entries WHERE (school <> 'Unattached') AND (Events = '100') OR (Events2 = '100') OR (Events3 = '100') GROUP BY school";
$result = $conn->query($sql);
I have tried using a union but it only gives me results for second query and the count of names is off.
$sql = "
SELECT COUNT(FName) as cnt, COUNT(Events + Events2 + Events3) as events, COUNT(DISTINCT(Gender)) as gnd, school
FROM entries WHERE (school <> 'Unattached')
UNION
SELECT COUNT(FName) as cnt, COUNT(Events + Events2 + Events3) as events, COUNT(DISTINCT(Gender)) as gnd, school
FROM entries WHERE (school <> 'Unattached') AND (Events = '100') OR (Events2 = '100') OR (Events3 = '100')
GROUP BY school";
$result = $conn->query($sql);
The left join gave me no results because I didn't know how to fit the 2 WHERE clauses.
How can I join the two queries to get one table with the following result
School
# of Participants
# Genders
# 100
Marymount
3
2
1
St Hughs
2
1
1
Campion
3
2
1
Use SUM() to add up the number of rows where a condition is true. This works because a boolean value is 1 when it's true, 0 when it's false, so SUM() gets this count.
SELECT school,
COUNT(*) AS num_participants,
COUNT(DISTINCT gender) AS num_genders,
SUM(events = '100' OR events2 = '100' OR events3 = '100') AS num_100
FROM entries
WHERE school != 'Unattached'
GROUP BY school

Multiple relationships in a stored procedure

I am preparing a query in which the total to be paid is added to the number of daily libraries that a student requests throughout the month.
It is worth mentioning that there are several types of libraries and what I need is to be able to associate the document payable to the game library catch of that month depending on the type of library
So, if a student requested 5 libraries in the month and 2 are morning and 3 are sports. 2 documents payable would be created and the morning document payable would be assigned to the 2 captures and the sports document to the 3 captures of the month.
CREATE PROCEDURE pagodeludotecaalumnos (IN alumnoid, IN fecha) BEGIN
insert into cj_documentoporpagar(documentoid, subconceptoid, pagoestatusid, alumnoid, cicloid, gradoid,
mediopagoid, importe, saldo, fechalimitepago, fechacreacion, fechaprontopago, referencia, documento,
hermanos, reingreso, padreexalumno, concepto, iva)
select 17 as DocumentoId,
case when a.TipoId = 1 or a.tipoid = 2 then 122
when a.TipoId = 3 then 235
when a.TipoId = 4 then -1 end as SubConceptoId,
1 as PagoEstatusId, b.AlumnoId,
5 as CicloId, b.GradoId, 1 as MedioPagoId,
case when c.PrimerNombre like '%*%' or c.ApellidoPaterno like '%*%' or c.ApellidoMaterno like '%*%' then 40 * Count(*) else 55 * Count(*) end as Importe,
case when c.PrimerNombre like '%*%' or c.ApellidoPaterno like '%*%' or c.ApellidoMaterno like '%*%' then 40 * Count(*) else 55 * Count(*) end as Saldo,
DATE(DATE_ADD(LAST_DAY(fecha), INTERVAL 1 MONTH)) as FechaLImitePago, now() as FechaCreacion, now() as FechaProntoPago,
'' as Referencia, '202003L' as documento, 0 as Hermanos, 0 as Reingreso, 0 as PadreExAlumno,
Concat('Ludoteca Marzo (', cast(Count(*) as int), ') dia(s)') as Concepto, 0 as IVA
/*, Concat(c.PrimerNombre, ' ', c.ApellidoPaterno, ' ', c.ApellidoMaterno) AlumnoNombre*/
from lu_captura a
inner join ce_alumnoporciclo b on a.alumnoporcicloid = b.alumnoporcicloid
inner join ce_alumno c on b.alumnoid = c.alumnoid
where date(a.Fecha) between DATE_FORMAT(fecha, '%Y-%m-01') and LAST_DAY(fecha) and a.TieneContrato = 0 and c.alumnoid = alumnoid
group by a.TipoId, b.AlumnoId, b.GradoId
SET #LID = LAST_INSERT_ID();
update lu_captura set DocumentoPorPagarId = #LID where Fecha between DATE_FORMAT(fecha, '%Y-%m-01') and LAST_DAY(fecha)
Currently I have this query and the question is: How could I relate the document payable created to the corresponding type of library?
According to MySQL Stored Procedure
I am using this syntax in stored procedure if you work in the same
then you can try this
DROP Procedure IF EXISTS pagodeludotecaalumnos;
Delimitter to used to set whole procedure below it as single operationable
DELIMITER $$
Create procedure command
CREATE PROCEDURE pagodeludotecaalumnos (
IN alumnoid int, IN fecha varchar(50), IN_action char(30)
)
Case begin from here
BEGIN
**-- switch case begin here**
CASE
WHEN IN_action = "insert" THEN
Creating entry for new record
INSERT INTO cj_documentoporpagar(
documentoid, subconceptoid,pagoestatusid, alumnoid, cicloid,
gradoid,mediopagoid, importe,saldo, fechalimitepago, fechacreacion,
fechaprontopago, referencia,documento,hermanos, reingreso,
padreexalumno, concepto, iva
)
VALUES(
IN_documentoid, IN_subconceptoid, IN_pagoestatusid,
IN_alumnoid,IN_cicloid,IN_gradoid, IN_mediopagoid, IN_importe,
IN_saldo,IN_fechalimitepago, IN_fechacreacion,IN_fechaprontopago,
IN_referencia, IN_documento, IN_hermanos, IN_reingreso,
IN_padreexalumno, IN_concepto, IN_iva
);
-- Insert end here
For update
WHEN IN_action = "update" THEN
-- For update elements
-- I am only show you how i make stored procedure in mysql i.e. your update statement
UPDATE lu_captura SET DocumentoPorPagarId = #LID where Fecha between
DATE_FORMAT(fecha, '%Y-%m-01') and LAST_DAY(fecha)
WHERE condition(if_exists);
For select statement
-- For Select elements
-- i.e. your select statement because i don't know what are you want to update, store an insert
WHEN IN_action = "select" THEN
select 17 as DocumentoId,
case when a.TipoId = 1 or a.tipoid = 2 then 122
when a.TipoId = 3 then 235
when a.TipoId = 4 then -1 end as
SubConceptoId,
1 as PagoEstatusId, b.AlumnoId,
5 as CicloId, b.GradoId, 1 as MedioPagoId,
case when c.PrimerNombre like '%*%' or c.ApellidoPaterno
like '%*%' or c.ApellidoMaterno
like '%*%' then 40 * Count(*)
else 55 * Count(*) end as Importe,
case
when c.PrimerNombre like '%*%' or c.ApellidoPaterno
like '%*%' or c.ApellidoMaterno
like '%*%' then 40 * Count(*) else 55 * Count(*)
end as Saldo,
DATE(DATE_ADD(LAST_DAY(fecha), INTERVAL 1 MONTH)) as
FechaLImitePago, now() as FechaCreacion, now() as
FechaProntoPago, '' as Referencia, '202003L' as documento, 0
as Hermanos, 0 as Reingreso, 0 as PadreExAlumno,
Concat('Ludoteca Marzo (', cast(Count(*) as int), ') dia(s)')
as Concepto, 0 as IVA
/*, Concat(c.PrimerNombre, ' ', c.ApellidoPaterno, ' ',
c.ApellidoMaterno) AlumnoNombre*/
from lu_captura a
inner join ce_alumnoporciclo b on a.alumnoporcicloid =
b.alumnoporcicloid
inner join ce_alumno c on b.alumnoid = c.alumnoid
where
date(a.Fecha) between DATE_FORMAT(fecha, '%Y-%m-01') and
LAST_DAY(fecha) and a.TieneContrato = 0 and c.alumnoid =
alumnoid group by a.TipoId, b.AlumnoId, b.GradoId
END CASE;
-- switch case ends here
END;
-- end of the PROCEDURE

PHP MYSQLI count users with two consecutive dates

How can I count users with two consecutive absences only? I have to create a chart to show frequency absences by employee.
My table name = incidencias
id | name | dateA | description
1 | al |2017-08-01| absence
2 | al |2017-08-02| absence
3 | alex |2017-08-01| absence
4 | alex |2017-08-02| absence
5 | alex |2017-08-03| absence
6 | al2 |2017-08-01| absence
7 | al2 |2017-08-02| absence
I want the result to be 2, only al and al2 have two consecutive dates where description = absence.
I´m using php to run the query, i did try this code i found but and I tested it in sqlfiddle and works great,. but not in my host.I think this is for PostgreSQL.
$query2 = mysqli_query($conn, "SELECT name,
sum(diff) as days,
(dateA) as work_start,
(dateA) as work_end
FROM (SELECT name,
dateA,
diff
FROM (select name,
dateA,
nvl(dateA- lag(dateA) over (partition by name order by dateA),1) as diff
from incidencias
where description = 'absence'
) t1
where diff = 1
) t2
group by name
having sum(diff) = 2");
$row_cnt = mysqli_num_rows($query2);
printf("Result set has %d rows.\n", $row_cnt);
I would really appreciate it.
So, this is normally done through JOINing on to the same table.
SELECT oinc.*
FROM incidencias oinc
LEFT JOIN
incidencias iinc
ON (oinc.name = iinc.name AND oinc.description = iinc.description)
WHERE description = 'absence'
AND oinc.dateA = DATE_ADD( iinc.dateA, 'INTERVAL 1 DAY');
So, line by line:
SELECT oinc.* -- grab me everything from the oinc table
FROM incidencias oinc -- We're going to call incidencias "oinc" in this query
-- "oinc" is now an alias for "incidencias"
LEFT JOIN -- I want a result whether or not the result is duplicated.
-- (Technically, by including the condition that it not be duplicated
-- this is the same thing as an "INNER JOIN".)
incidencias iinc -- We're looking at the same table, better call it something else
ON (oinc.name = iinc.name AND oinc.description = iinc.description)
-- We're matching the name and the description between the two
-- aliases of the table (oinc, iicn)
WHERE description = 'absence' -- Should be obvious
AND oinc.dateA = DATE_ADD( iinc.dateA, 'INTERVAL 1 DAY'); -- the iinc alias
-- has a date which is one day less than the oinc alias
Some side notes:
I used left join so that you can omit the AND ... later.
You should experiment with moving that AND query from the WHERE into the ON clause. Then you can use an INNER join. You'll get the same results, but knowing both will help you more later.
Here's one way (there may be a simpler solution, but this should be fast anyway)...
SELECT COUNT(*)
FROM
( SELECT name
, MAX(i) i
FROM
( SELECT x.*
, CASE WHEN #prev_name = name THEN
CASE WHEN #prev_date = datea - INTERVAL 1 DAY THEN #i:=#i+1 ELSE #i:=1 END
ELSE #i:=1 END i
, #prev_name := name
, #prev_date := datea
FROM my_table x
, ( SELECT #prev_name:=null,#prev_date:=null, #i:=1) vars
WHERE x.description = 'absence'
ORDER
BY name
, datea
) b
GROUP
BY name
HAVING i = 2
) p;

Retrieving available rooms and their beds for a given date range from database

I have read carefuly through all the posts on here about this topic, but my question has one extra catch, where I need to get available beds of each room because its a hostel and some rooms are shared.
Now I have tried doing this in PHP, but then realized I haven't even taken in account the date range. So now I am thinking since I already have a query that retrieves all reservations occurring within a given date range and then compare the retrieved rooms and their beds with the room table and show only rooms and their beds that are not occupied. But I can't figure out how to work around the beds because they are not an entity, only a count of total beds in each room. But then a reservation says for which bed in the room that reservation is made..
Here are my tables
Rooms
Reservations
Now the query that I use to retrieve all reserved rooms and their beds is
SELECT rooms_id, bed
FROM reservations
WHERE `to` > '2016-02-18' AND `from` < '2016-02-24'
The first date if my input variable $from and the second date is input variable $to and it allows to retrieve not only rooms that have a reservation that starts within $from and $to but also all reservations that started before the date range and end inside, started inside and end after the date range and finally reservations that started before and end after the date range. So this exact query above would return the following table
which I can then visualize in my app like this
But this is where I get stuck. I have no idea how to match the data I have in order to find all available rooms BUT ALSO THE BEDS.
The desired table AVAILABLE ROOMS for the date range from '2016-02-18' and to '2016-02-24' should look like this:
|||||||||||||||||||||||||||||||||
|| rooms_id || bed_number ||
||||||||||||||||||||||||||||
|| 1 || 1 ||
----------------------------
|| 1 || 2 ||
----------------------------
|| 2 || 5 ||
----------------------------
|| 2 || 6 ||
----------------------------
|| 2 || 7 ||
----------------------------
|| 2 || 8 ||
----------------------------
You can see this in the picture where I show how it looks in my app. The only rooms and beds that are available between the two dates are the Luxury Room and its bed number 1 and bed number 2 and the Dorm rooms beds 5,6,7,8 because 1-4 have a reservation occurring on at least on one of the desired dates
The only idea I had was using NOT IN, but that only works if I didn't care about the beds and also the output is of here it is
SELECT *
FROM `rooms`
WHERE `id` NOT IN
(SELECT rooms_id FROM reservations WHERE `to` > '2016-02-18' AND `from` < '2016-02-24')
instead of what I "sketched" above
I'd appreciate any tips and ideas on how approach this.
Part of me worries that this will all come down to me not treating the beds as entities and having to do that, even though as I will never be storing any sort of information on the beds such as their color, position, quality, price, etc...
REPLY TO #Paul-Spiegel
That is amazing, but is there any way to get the free beds as numbers as well instead of total number. Because then when the person makes a reservation I have to assign it to on of the beds. So if the result could be
| room_id | title | beds_total | available_bed_nrs |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 1 | luxury room | 2 | 1, 2 |
| 2 | dorm room | 8 | 5, 6, 7, 8 |
instead of
You can get a number of free rooms (and the reserved room numbers) with this query:
set #from := '2016-02-18';
set #to := '2016-02-24';
set #beds := 1;
SELECT rm.id, rm.title, rm.beds,
rm.beds - IFNULL(rv.num_reserved_beds, 0) AS num_free_beds,
rv.reserved_bed_nrs
FROM rooms rm
LEFT JOIN (
SELECT rv.rooms_id,
COUNT(1) as num_reserved_beds,
GROUP_CONCAT(rv.bed) as reserved_bed_nrs
FROM reservations rv
WHERE rv.from < #to
AND rv.to > #from
GROUP BY rv.rooms_id
) rv ON rv.rooms_id = rm.id
HAVING num_free_beds >= #beds
You can now parse reserved_bed_nrs, loop over all beds per room and pick the beds that are not in reserved_bed_nrs.
Explaination:
Get all beds reserved within date range (excluding):
SELECT *
FROM reservations r
WHERE r.from < #to
AND r.to > #from;
Group by room, count the number of reserved rooms and store all numbers of reserved rooms in one string field:
SELECT rv.rooms_id,
COUNT(1) as num_reserved_beds,
GROUP_CONCAT(rv.bed) as reserved_bed_nrs
FROM reservations rv
WHERE rv.from < #to
AND rv.to > #from
GROUP BY rv.rooms_id
Join (LEFT JOIN) rooms with the given result calculate the number of free beds and compare it with the number of beds you want to book.
Update How to get free (not reserved) beds:
If you don't have a table with all existing beds, you will need some kind of sequece numbers. Assuming a room can have a maximum of 100 beds you can create a sequence table with 100 numbers:
CREATE TABLE `sequence` (
`nr` TINYINT(3) UNSIGNED NOT NULL,
PRIMARY KEY (`nr`)
) select d1.d*10 + d0.d + 1 as nr from
(select 0 d 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) d0,
(select 0 d 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) d1
Now it's possible to list all existing beds by cross joining the tables rooms and sequence:
SELECT *
FROM rooms rm
CROSS JOIN sequence seq
WHERE seq.nr <= rm.beds
To list all not resverd beds you can combine it with a query for reserved beds (select all beds that are not reserved within the booking date range):
SELECT *
FROM rooms rm
CROSS JOIN sequence seq
WHERE seq.nr <= rm.beds
AND (rm.id, seq.nr) NOT IN (
SELECT rv.rooms_id, rv.bed
FROM reservations rv
WHERE rv.from < '2016-02-24'
AND rv.to > '2016-02-18'
)
This can also be done with NOT EXISTS or excluding LEFT JOIN.
You also can skip the creation of the sequence table use the creation code as subselect:
SELECT *
FROM rooms rm
CROSS JOIN (
select d1.d*10 + d0.d + 1 as nr
from
(select 0 d 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) d0,
(select 0 d 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) d1
) seq
WHERE seq.nr <= rm.beds
AND (rm.id, seq.nr) NOT IN (
SELECT rv.rooms_id, rv.bed
FROM reservations rv
WHERE rv.from < '2016-02-24'
AND rv.to > '2016-02-18'
)
http://sqlfiddle.com/#!9/a0d61/5

Fetch range from days

I have this table structure:
EDIT more complex example: add hidden range
category| day | a |
--------|------------|-------|
1 | 2012-01-01 | 4 |
1 | 2012-01-02 | 4 |
1 | 2012-01-03 | 4 |
1 | 2012-01-04 | 4 |
1 | 2012-01-05 | 5 |
1 | 2012-01-06 | 5 |
1 | 2012-01-07 | 5 |
1 | 2012-01-08 | 4 |
1 | 2012-01-09 | 4 |
1 | 2012-01-10 | 4 |
1 | 2012-01-11 | 5 |
1 | 2012-01-12 | 5 |
1 | 2012-01-16 | 5 |
1 | 2012-01-17 | 5 |
1 | 2012-01-18 | 5 |
1 | 2012-01-19 | 5 |
...
with 'category-day' as unique keys. I would extract a range of dates, for each category, according with column "a" and given limit range, like so:
1,2012-01-01|2012-01-04,4
1,2012-01-05|2012-01-07,5
1,2012-01-08|2012-01-10,4
1,2012-01-11|2012-01-12,5
1,2012-01-13|2012-01-15,0
1,2012-01-16|2012-01-19,5
or similar.
I search the best way for do it. Using only mysql preferably but also with a little bit of php.
NOTE1: not all day are inserted: between two days non-contiguos could not be other days. In this case I would in output the missed range with column "a" = 0.
NOTE2: I did it with a simple query and some rows of php but I don't like it because my simple algorithm need a cycle for each day in range multiplied for each category found. If range is too big and there are too much categories, that's not so good.
FINAL EDIT: OK! After reading all comments and answers, I think not exists a valid, efficient and, at same time, readable solution. So Mosty Mostacho answer is a no 100% valid solution, but it has 100% valid suggestions. Thank you all.
New edit:
As I told you in a comment, I strongly recommend you to use the quick query and then process the missing dates in PHP as that would be faster and more readable:
select
concat(#category := category, ',', min(day)) col1,
concat(max(day), ',', #a := a) col2
from t, (select #category := '', #a := '', #counter := 0) init
where #counter := #counter + (category != #category or a != #a)
group by #counter, category, a
However, if you still want to use the query version, then try this:
select
#counter := #counter + (category != #category or a != #a) counter,
concat(#category := category, ',', min(day)) col1,
concat(max(day), ',', #a := a) col2
from (
select distinct s.day, s.category, coalesce(t1.a, 0) a
from (
select (select min(day) from t) + interval val - 1 day day, c.category
from seq s, (select distinct category from t) c
having day <= (select max(day) from t)
) s
left join t t1 on s.day = t1.day and s.category = t1.category
where s.day between (
select min(day) from t t2
where s.category = t2.category) and (
select max(day) from t t2
where s.category = t2.category)
order by s.category, s.day
) t, (select #category := '', #a := '', #counter := 0) init
group by counter, category, a
order by category, min(day)
Note that MySQL won't allow you to create data on the fly, unless you hardcode UNIONS, for example. This is an expensive process that's why I strongly suggest you to create a table with only an integer field with values from 1 to X, where X is, at least the maximum amount of dates that separate the min(day) and max(day) from your table. If you're not sure about that date, just add 100,000 numbers and you'll be able to generate range periods for over 200 years. In the previous query, this table is seq and the column it has is val.
This results in:
+--------------+--------------+
| COL1 | COL2 |
+--------------+--------------+
| 1,2012-01-01 | 2012-01-04,4 |
| 1,2012-01-05 | 2012-01-07,5 |
| 1,2012-01-08 | 2012-01-10,4 |
| 1,2012-01-11 | 2012-01-12,5 |
| 1,2012-01-13 | 2012-01-15,0 |
| 1,2012-01-16 | 2012-01-19,5 |
+--------------+--------------+
Ok, I'm lying. The result is actually returning a counter column. Just disregard it, as removing it (using a derived table) would be even less performant!
and here's a one liner brutality for you :) (Note: Change the "datt" table name.)
select dd.category,
dd.day as start_day,
(select dp.day from
(
select 1 as n,d1.category,d1.day,d1.a from datt d1 where not exists (
select * from datt where day = d1.day - INTERVAL 1 DAY and a=d1.a
)
union
select 2 as n,d1.category,d1.day,d1.a from datt d1 where not exists (
select * from datt where day = d1.day + INTERVAL 1 DAY and a=d1.a
)
) dp where dp.day >= dd.day - INTERVAL (n-2) DAY order by day asc limit 0,1)
as end_day,
dd.a from (
select 1 as n,d1.category,d1.day,d1.a from datt d1 where not exists (
select * from datt where day = d1.day - INTERVAL 1 DAY and a=d1.a
)
union
select 2 as n,d1.category,d1.day,d1.a from datt d1 where not exists (
select * from datt where day = d1.day + INTERVAL 1 DAY and a=d1.a
)
) dd
where n=1
and it's output is :
|| 1 || 2012-01-01 || 2012-01-01 || 4 ||
|| 1 || 2012-01-03 || 2012-01-04 || 4 ||
|| 1 || 2012-01-05 || 2012-01-07 || 5 ||
|| 1 || 2012-01-08 || 2012-01-10 || 4 ||
|| 1 || 2012-01-11 || 2012-01-12 || 5 ||
Note: Thats the result for non-existing 2012-01-02 in a 01-12 day table.
No need for PHP or temporary tables or anything.
DISCLAIMER: I did this just for fun. This stunt may be too crazy to be used in a production environment. Therefore I'm not posting this as a "real" solution. Also I'm not willing to explain how it works :) And I didn't rethink / refactor it. There might be more elegant ways and names / aliases could be more informative. So please no flame or anything.
Here's my solution. Looks more complicated than it is. I think it may be easier to understand than other answers, no offense :)
Setting up test data:
drop table if exists test;
create table test(category int, day date, a int);
insert into test values
(1 , '2012-01-01' , 4 ),
(1 , '2012-01-02' , 4 ),
(1 , '2012-01-03' , 4 ),
(1 , '2012-01-04' , 4 ),
(1 , '2012-01-05' , 5 ),
(1 , '2012-01-06' , 5 ),
(1 , '2012-01-07' , 5 ),
(1 , '2012-01-08' , 4 ),
(1 , '2012-01-09' , 4 ),
(1 , '2012-01-10' , 4 ),
(1 , '2012-01-11' , 5 ),
(1 , '2012-01-12' , 5 ),
(1 , '2012-01-16' , 5 ),
(1 , '2012-01-17' , 5 ),
(1 , '2012-01-18' , 5 ),
(1 , '2012-01-19' , 5 );
And here it comes:
SELECT category, MIN(`day`) AS firstDayInRange, max(`day`) AS lastDayInRange, a
, COUNT(*) as howMuchDaysInThisRange /*<-- as a little extra*/
FROM
(
SELECT
IF(#prev != qr.a, #is_a_changing:=#is_a_changing+1, #is_a_changing) AS is_a_changing, #prev:=qr.a, qr.* /*See if column a has changed. If yes, increment, so we can GROUP BY it later*/
FROM
(
SELECT
test.category, q.`day`, COALESCE(test.a, 0) AS a /*When there is no a, replace NULL with 0*/
FROM
test
RIGHT JOIN
(
SELECT
DATE_SUB(CURDATE(), INTERVAL number_days DAY) AS `day` /*<-- Create dates from now back 999 days. This query is surprisingly fast. And adding more numbers to create more dates, i.e. 10000 dates is also no problem. Therefor a temporary dates table might not be necessary?*/
FROM
(
SELECT (a + 10*b + 100*c) AS number_days 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) aa
, (SELECT 0 AS b 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) bb
, (SELECT 0 AS c 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) cc
)sq /*<-- This generates numbers 0 to 999*/
)q USING(`day`)
, (SELECT #is_a_changing:=0, #prev:=0) r
/*This WHERE clause is just to beautify. It may not be necessary*/
WHERE q.`day` >= (SELECT MIN(test.`day`) FROM test) AND q.`day` <= (SELECT MAX(test.`day`) FROM test)
)qr
)asdf
GROUP BY is_a_changing
ORDER BY 2
Result looks like this:
category firstDayInRange lastDayInRange a howMuchDaysInThisRange
--------------------------------------------------------------------------
1 2012-01-01 2012-01-04 4 4
1 2012-01-05 2012-01-07 5 3
1 2012-01-08 2012-01-10 4 3
1 2012-01-11 2012-01-12 5 2
2012-01-13 2012-01-15 0 3
1 2012-01-16 2012-01-19 5 4
To make this work as you want it to, you should have two tables:
for periods
for days
Where each period can have many days related to it through FOREIGN KEY. With current table structure, the best you can do is to detect the continuous periods on PHP side.
Firstly, this is an extension of #Mosty's solution.
To enable Mosty's solution to include category/date combinations than do not exist in the table I took the following approach -
Start by getting a distinct list of categories and then join this to the entire date range -
SELECT category, `start` + INTERVAL id DAY AS `day`
FROM dummy,(SELECT DISTINCT category FROM t) cats, (SELECT MIN(day) `start`, MAX(day) `end` FROM t) tmp
WHERE id <= DATEDIFF(`end`, `start`)
ORDER BY category, `day`
The above query builds the full date range using the table dummy with a single field id. The id field contains 0,1,2,3,.... - it needs to have enough values to cover every day in the required date range. This can then be joined back to the original table to create a complete list of all categories for all dates and the appropriate value for a -
SELECT cj.category, cj.`day`, IFNULL(t.a, 0) AS a
FROM (
SELECT category, `start` + INTERVAL id DAY AS `day`
FROM dummy,(SELECT DISTINCT category FROM t) cats, (SELECT MIN(day) `start`, MAX(day) `end` FROM t) tmp
WHERE id <= DATEDIFF(`end`, `start`)
ORDER BY category, `day`
) AS cj
LEFT JOIN t
ON cj.category = t.category
AND cj.`day` = t.`day`
This can then be applied to Mosty's query in place of table t -
SELECT
CONCAT(#category := category, ',', MIN(`day`)) col1,
CONCAT(MAX(`day`), ',', #a := a) col2
FROM (
SELECT cj.category, cj.day, IFNULL(t.a, 0) AS a
FROM (
SELECT category, `start` + INTERVAL id DAY AS `day`
FROM dummy,(SELECT DISTINCT category FROM t) cats, (SELECT MIN(day) `start`, MAX(day) `end` FROM t) tmp
WHERE id <= DATEDIFF(`end`, `start`)
ORDER BY category, `day`
) AS cj
LEFT JOIN t
ON cj.category = t.category
AND cj.`day` = t.day) AS t, (select #category := '', #a := '', #counter := 0) init
WHERE #counter := #counter + (category != #category OR a != #a)
GROUP BY #counter, category, a
Completely on mysql side will have performance adv:
Once the procedure has been created, it runs within 0.35 - 0.37 sec
create procedure fetch_range()
begin
declare min date;
declare max date;
create table testdate(
date1 date
);
select min(day) into min
from category;
select max(day) into max
from category;
while min <= max do
insert into testdate values(min);
set min = adddate(min,1);
end while;
select concat(category,',',min(day)),concat(max(day),',',a)
from(
SELECT if(isNull(category),#category,category) category,if(isNull(day),date1,day) day,#a,if(isNull(a) || isNull(#a),if(isNull(a) && isNull(#a),#grp,#grp:=#grp+1),if(#a!=a,#grp:=#grp+1,#grp)) as sor_col,if(isNull(a),0,a) as a,#a:=a,#category:= category
FROM `category`
RIGHT JOIN testdate ON date1 = category.day) as table1
group by sor_col;
drop table testdate;
end
o/p:
1,2012-01-01|2012-01-04,4
1,2012-01-05|2012-01-07,5
1,2012-01-08|2012-01-10,4
1,2012-01-11|2012-01-12,5
1,2012-01-13|2012-01-15,0
1,2012-01-16|2012-01-19,5
Here is mysql solution which will give the desired result excluding the missed range only.
PHP:
The missing range can be added through php.
$sql = "set #a=0,#grp=0,#datediff=0,#category=0,#day='';";
mysql_query($sql);
$sql= "select category,min(day)min,max(day) max,a
from(
select category,day,a,concat(if(#a!=a,#grp:=#grp+1,#grp),if(datediff(#day,day) < -1,#datediff:=#datediff+1,#datediff)) as grp_datediff,datediff(#day,day)diff, #day:= day,#a:=a
FROM category
order by day)as t
group by grp_datediff";
$result = mysql_query($sql);
$diff = 0;
$indx =0;
while($row = mysql_fetch_object($result)){
if(isset($data[$indx - 1]['max'])){
$date1 = new DateTime($data[$indx - 1]['max']);
$date2 = new DateTime($row->min);
$diff = $date1->diff($date2);
}
if ($diff->days > 1) {
$date = new DateTime($data[$indx-1]['max']);
$interval = new DateInterval("P1D");
$min = $date->add($interval);
$date = new DateTime($data[$indx-1]['max']);
$interval = new DateInterval("P".$diff->days."D");
$max = $date->add($interval);
$data[$indx]['category'] = $data[$indx-1]['category'];
$data[$indx]['min'] = $min->format('Y-m-d');
$data[$indx]['max'] = $max->format('Y-m-d');
$data[$indx++]['a'] = 0;
$data[$indx]['category'] = $row->category;
$data[$indx]['min'] = $row->min;
$data[$indx]['max'] = $row->max;
$data[$indx]['a'] = $row->a;
}else{
$data[$indx]['category'] = $row->category;
$data[$indx]['min'] = $row->min;
$data[$indx]['max'] = $row->max;
$data[$indx]['a'] = $row->a;
}
$indx++;
}
Is this what you mean?
SELECT
category,
MIN(t1.day),
MAX(t2.day),
a
FROM
`table` AS t1
INNER JOIN `table` AS t2 USING (category, a)
If I understand your question correctly, I would use something to the effect of:
SELECT MAX(day), MIN(day) FROM `YourTable` WHERE `category`= $cat AND `A`= $increment;
... and ...
$dateRange = $cat.","."$min"."|"."$max".",".$increment;

Categories