Как комментарий к моему вопросу на Возможно ли этот запрос, который пытается получить статусы временных рядов с усеченными датами, даже в обычных реляционных базах данных? Я реализовал запрос временных рядов на postgres, который работает достаточно хорошо , Он выравнивает время на целые периоды (например, дни) и объединяет его с некоторыми данными.
Тем не менее, существует серьезная проблема: запрос зависит от часового пояса, который работает нормально, но когда летнее время (DST) происходит в середине сгенерированного ряда, это не отражается в выходных данных. К сожалению, в некоторых часовых поясах 1 день в году занимает всего 23 часа, а другой - 25 часов. Мне нужно, чтобы данные были агрегированы за этот период 23 или 25 часов, потому что это истинные календарные дни в этом часовом поясе. Но с текущим запросом он просто всегда добавляет 1 день к серии. Это означает, что во время перехода на летнее время я получаю выходные данные с такими данными:
date 1: 00:00
date 2: 00:00
date 3: 00:00
(now a DST change happens)
date 3: 23:00
date 4: 23:00
... and so on
Я не знаю, как переписать этот запрос, чтобы учесть, что определенные дни занимают меньше или больше часов в некоторых часовых поясах. Потому что generate_series основан на интервалах. Есть идеи? Фактический код имеет произвольный период и сумму между прочим, это также может быть 5 месяцев или 3 часа.
Вот полный запрос, хотя я думаю, что релевантным является только запрос sub1.
SELECT sub2.fromdate,
sub2.eventlevel,
sub2.count
FROM
(SELECT sub1.fromdate AS fromdate,
sub1.maxeventlevel AS eventlevel,
count(*) AS COUNT
FROM
(SELECT e.subject_id,
MAX(e.event_level) AS maxeventlevel,
d.date AS fromdate
FROM
(SELECT generate_series(date_trunc(?, ? AT TIME ZONE ?) AT TIME ZONE ?, date_trunc(?, ? AT TIME ZONE ?) AT TIME ZONE ? , interval '1' DAY)) d(date)
INNER JOIN event e ON ((e.end_date > d.date
AND e.end_date > ?)
OR e.end_date IS NULL)
AND e.date < d.date + interval '1' DAY
AND e.date < ?
AND d.date < ?
INNER JOIN subject ON subject.id = e.subject_id
INNER JOIN metric ON metric.id = e.metric_id
INNER JOIN event_configuration_version ON event_configuration_version.id = e.event_configuration_version_id
INNER JOIN event_configuration ON event_configuration.id = event_configuration_version.event_configuration_id
WHERE subject.project_id = ?
GROUP BY e.subject_id,
fromdate) AS sub1
GROUP BY sub1.fromdate,
sub1.maxeventlevel) AS sub2
ORDER BY sub2.fromdate,
sub2.eventlevel DESC
Я не думаю, что смогу что-либо сделать в коде после того, как запрос уже выполнен, но я открыт для любых пропущенных решений кода, хотя в идеале мы получаем правильные результаты обратно из самого SQL-запроса. , Нам нужно выполнить большую часть агрегации в самой базе данных, но если есть что-то умное, что можно сделать в другом месте, то это тоже работает. Код Java, генерирующий и выполняющий этот запрос и преобразующий результат, запускается в приложении Spring Boot и выглядит следующим образом:
public PeriodAggregationDTO[] getSubjectStatesReport(
AggregationPeriod aggregationPeriod, Integer aggregationPeriodAmount, UUID projectId,
List<UUID> eventTriggerIds, List<UUID> subjectIds, List<UUID> metricIds, List<EventLevel> eventLevels,
Date fromDate, Date toDate) {
// to avoid an even more complex native query, we obtain the project here so a) we are sure
// that this user has access
// and b) we can get the timezone already without additional joins later.
Project project = serviceUtil.findProjectByIdOrThrowApiException(projectId);
String timezoneId = project.getTimezoneId();
boolean skipEventTriggers = eventTriggerIds == null || eventTriggerIds.size() == 0;
boolean skipSubjects = subjectIds == null || subjectIds.size() == 0;
boolean skipMetrics = metricIds == null || metricIds.size() == 0;
boolean skipEventLevels = eventLevels == null || eventLevels.size() == 0;
StringBuilder whereClause = new StringBuilder();
whereClause.append(" WHERE subject.project_id = :projectId");
if (!skipEventTriggers) {
whereClause.append(" AND event_trigger.id in :eventTriggerIds");
}
if (!skipSubjects) {
whereClause.append(" AND subject_id in :subjectIds");
}
if (!skipMetrics) {
whereClause.append(" AND metric.id in :metricIds");
}
if (!skipEventLevels) {
whereClause.append(" AND e.event_level in :eventLevels");
}
String interval = String.format("'%d' %s", aggregationPeriodAmount, aggregationPeriod);
String series = "SELECT generate_series("
+ "date_trunc(:period, :fromDate AT TIME ZONE :timezoneId) AT TIME ZONE :timezoneId"
+ " , date_trunc(:period, :toDate AT TIME ZONE :timezoneId) AT TIME ZONE :timezoneId"
+ " , interval " + interval + ")";
String innersubquery = "SELECT e.subject_id" + ",MAX(e.event_level) as maxeventlevel"
+ ",d.date as fromdate"
+ " FROM (" + series + " ) d(date)"
+ " INNER JOIN event e ON ((e.end_date > d.date AND e.end_date > :fromDate)"
+ " OR e.end_date IS NULL) AND e.date < d.date + interval " + interval
+ " AND e.date < :toDate AND d.date < :toDate"
+ " INNER JOIN subject ON subject.id = e.subject_id"
+ " INNER JOIN metric ON metric.id = e.metric_id"
+ " INNER JOIN event_trigger_version ON event_trigger_version.id = e.event_trigger_version_id"
+ " INNER JOIN event_trigger ON event_trigger.id = event_trigger_version.event_trigger_id"
+ whereClause.toString()
+ " GROUP BY e.subject_id, fromdate";
String outersubquery = "SELECT" + " sub1.fromdate as fromdate"
+ ",sub1.maxeventlevel as eventlevel" + ",count(*) as count" + " FROM"
+ " (" + innersubquery + ") AS sub1"
+ " GROUP BY sub1.fromdate, sub1.maxeventlevel";
String queryString = "SELECT sub2.fromdate, sub2.eventlevel, sub2.count FROM ("
+ outersubquery + ") AS sub2"
+ " ORDER BY sub2.fromdate, sub2.eventlevel DESC";
Query query = em.createNativeQuery(queryString);
query.setParameter("projectId", projectId);
query.setParameter("timezoneId", timezoneId);
query.setParameter("period", aggregationPeriod.toString());
query.setParameter("fromDate", fromDate);
query.setParameter("toDate", toDate);
if (!skipEventTriggers) {
query.setParameter("eventTriggerIds", eventTriggerIds);
}
if (!skipSubjects) {
query.setParameter("subjectIds", subjectIds);
}
if (!skipMetrics) {
query.setParameter("metricIds", metricIds);
}
if (!skipEventLevels) {
List<Integer> eventLevelOrdinals =
eventLevels.stream().map(Enum::ordinal).collect(Collectors.toList());
query.setParameter("eventLevels", eventLevelOrdinals);
}
List<?> resultList = query.getResultList();
Stream<AggregateQueryEntity> stream = resultList.stream().map(obj -> {
Object[] array = (Object[]) obj;
Timestamp timestamp = (Timestamp) array[0];
Integer eventLevelOrdinal = (Integer) array[1];
EventLevel eventLevel = EventLevel.values()[eventLevelOrdinal];
BigInteger count = (BigInteger) array[2];
return new AggregateQueryEntity(timestamp, eventLevel, count.longValue());
});
return transformQueryResult(stream);
}
private PeriodAggregationDTO[] transformQueryResult(Stream<AggregateQueryEntity> stream) {
// we specifically use LinkedHashMap to maintain ordering. We also set Linkedlist explicitly
// because there are no guarantees for this list type with toList()
Map<Timestamp, List<AggregateQueryEntity>> aggregatesByDate = stream
.collect(Collectors.groupingBy(AggregateQueryEntity::getTimestamp,
LinkedHashMap::new, Collectors.toCollection(LinkedList::new)));
return aggregatesByDate.entrySet().stream().map(entryByDate -> {
PeriodAggregationDTO dto = new PeriodAggregationDTO();
dto.setFromDate((Date.from(entryByDate.getKey().toInstant())));
List<AggregateQueryEntity> value = entryByDate.getValue();
List<EventLevelAggregationDTO> eventLevelAggregationDTOS = getAggregatesByEventLevel(value);
dto.setEventLevels(eventLevelAggregationDTOS);
return dto;
}).toArray(PeriodAggregationDTO[]::new);
}
private List<EventLevelAggregationDTO> getAggregatesByEventLevel(
List<AggregateQueryEntity> value) {
Map<EventLevel, AggregateQueryEntity> aggregatesByEventLevel = value.stream()
.collect(Collectors.toMap(AggregateQueryEntity::getEventLevel, Function.identity(), (u, v) -> {
throw new InternalException(String.format("Unexpected duplicate event level %s", u));
}, LinkedHashMap::new));
return aggregatesByEventLevel.values().stream().map(aggregateQueryEntity -> {
EventLevelAggregationDTO eventLevelAggregationDTO = new EventLevelAggregationDTO();
eventLevelAggregationDTO.setEventLevel(aggregateQueryEntity.getEventLevel());
eventLevelAggregationDTO.setCount(aggregateQueryEntity.getCount());
return eventLevelAggregationDTO;
}).collect(Collectors.toCollection(LinkedList::new));
}
С другим классом данных:
@Data
class AggregateQueryEntity {
private final Timestamp timestamp;
private final EventLevel eventLevel;
private final long count;
}