从大桌子中选择好的点差

时间:2017-05-10 12:11:06

标签: sql postgresql time-series nearest-neighbor postgresql-performance

我正在尝试编写一个存储过程,用于从一个大表中及时选择X个扩散点数量。

我有一张表points

  "Userid" integer
, "Time"   timestamp with time zone
, "Value"  integer 

它包含数亿条记录。每个用户大约有一百万条记录。

我想选择X点(比方说50),这些点从时间A到时间B都很好地扩散。问题是点数不均匀分布(如果一个点在6:00:00,下一个点例如,可以在15秒,20或4分钟后点。

选择身份证的所有积分最多可能需要60秒(因为有大约一百万点)。

有没有办法以快速的方式尽可能多地选择我想要的确切点数?

示例数据:

   +--------+---------------------+-------+
   | UserId | Time                | Value |
   +--------+---------------------+-------+
1  | 1      | 2017-04-10 14:00:00 | 1     |
2  | 1      | 2017-04-10 14:00:10 | 10    |
3  | 1      | 2017-04-10 14:00:20 | 32    |
4  | 1      | 2017-04-10 14:00:35 | 80    |
5  | 1      | 2017-04-10 14:00:58 | 101   |
6  | 1      | 2017-04-10 14:01:00 | 203   |
7  | 1      | 2017-04-10 14:01:30 | 204   |
8  | 1      | 2017-04-10 14:01:40 | 205   |
9  | 1      | 2017-04-10 14:02:02 | 32    |
10 | 1      | 2017-04-10 14:02:15 | 7     |
11 | 1      | 2017-04-10 14:02:30 | 900   |
12 | 1      | 2017-04-10 14:02:45 | 22    |
13 | 1      | 2017-04-10 14:03:00 | 34    |
14 | 1      | 2017-04-10 14:03:30 | 54    |
15 | 1      | 2017-04-10 14:04:00 | 54    |
16 | 1      | 2017-04-10 14:06:00 | 60    |
17 | 1      | 2017-04-10 14:07:20 | 654   |
18 | 1      | 2017-04-10 14:07:40 | 32    |
19 | 1      | 2017-04-10 14:08:00 | 33    |
20 | 1      | 2017-04-10 14:08:12 | 32    |
21 | 1      | 2017-04-10 14:10:00 | 8     |
   +--------+---------------------+-------+

对于ID为1的用户,我想从上面的列表中选择11个“最佳”点, 从时间2017-04-10 14:00:00到2017-04-10 14:10:00。

目前,在为用户选择所有点后,在服务器上完成了它。 我通过除以时间差来计算“最佳时间”并获得如下列表:14:00:00,14:01:00,...... 14:10:00(11“最佳时间”,作为分数)。比我找到每个“最佳时间”的最近点,还没有被选中。 结果将是分数:1,6,9,13,15,16,17,18,19,20,21

编辑:

我正在尝试这样的事情:

SELECT * FROM "points"
WHERE "Userid" = 1 AND
(("Time" =
(SELECT "Time" FROM 
"points"
ORDER BY abs(extract(epoch from '2017-04-10 14:00:00' - "Time"))
LIMIT 1)) OR
("Time" =
(SELECT "Time" FROM 
"points"
ORDER BY abs(extract(epoch from '2017-04-10 14:01:00' - "Time"))
LIMIT 1)) OR
("Time" =
(SELECT "Time" FROM 
"points"
ORDER BY abs(extract(epoch from '2017-04-10 14:02:00' - "Time"))
LIMIT 1)))

这里的问题是:
A)不考虑已经选择的点数 B)由于ORDER BY,每增加一次会使查询的运行时间增加约1秒,而对于50分,我会回到1分钟。

2 个答案:

答案 0 :(得分:0)

回答使用generate_series('2017-04-10 14:00:00','2017-04-10 14:10:00','1 minute'::interval)join进行比较。

让其他人节省数据集的时间:

t=# create table points(i int,"UserId" int,"Time" timestamp(0), "Value" int,b text);
CREATE TABLE
Time: 13.728 ms
t=# copy points from stdin delimiter '|';
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself.
>> 1  | 1      | 2017-04-10 14:00:00 | 1     |
>> 2  | 1      | 2017-04-10 14:00:10 | 10    |
3  | 1      | 2017-04-10 14:00:20 | 32    |
4  | 1      | 2017-04-10 14:00:35 | 80    |
5  | 1      | 2017-04-10 14:00:58 | 101   |
6  | 1      | 2017-04-10 14:01:00 | 203   |
7  | 1      | 2017-04-10 14:01:30 | >> 204   |
8  | 1      | 2017-04-10 14:01:40 | 205   |
9  | 1      | 2017-04-10 14:02:02 | 32    |
10 | 1      | 2017-04-10 14:02:15 | 7     |
11 | 1      | 2017-04-10 14:02:30 | 900   |
12 | 1      | 2017-04-10 14:02:45 | 22    |
>> >> >> >> >> >> >> >> >> >> 13 | 1      | 2017-04-10 14:03:00 | 34    |
14 | 1      | 2017-04-10 14:03:30 | 54    |
15 | 1      | 2017-04-10 14:04:00 | 54    |
16 | 1      | 2017-04-10 14:06:00 | 60    |
17 | 1      | 2017-04-10 14:07:20 | 654   |
18 | 1      | 2017-04-10 14:07:40 | 32    |
19 | 1      | 2017-04-10 14:08:00 | 33    |
20 | 1      | 2017-04-10 14:08:12 | 32    |
21 | 1      | 2017-04-10 14:10:00 | 8     |>> >> >> >> >> >> >> >> \.
>> \.
COPY 21
Time: 7684.259 ms
t=# alter table points rename column "UserId" to "Userid";
ALTER TABLE
Time: 1.013 ms

坦白说我不明白这个要求。这就是我从描述中得到的结果,结果与OP的预期结果不同:

t=# with r as (
  with g as (
    select generate_series('2017-04-10 14:00:00','2017-04-10 14:10:00','1 minute'::interval) s
  )
  select *,abs(extract(epoch from '2017-04-10 14:02:00' - "Time"))
  from g
  join points on g.s = date_trunc('minute',"Time")
  order by abs
  limit 11
)
select i, "Time","Value",abs
from r
order by i;
 i  |        Time         | Value | abs
----+---------------------+-------+-----
  4 | 2017-04-10 14:00:35 |    80 |  85
  5 | 2017-04-10 14:00:58 |   101 |  62
  6 | 2017-04-10 14:01:00 |   203 |  60
  7 | 2017-04-10 14:01:30 |   204 |  30
  8 | 2017-04-10 14:01:40 |   205 |  20
  9 | 2017-04-10 14:02:02 |    32 |   2
 10 | 2017-04-10 14:02:15 |     7 |  15
 11 | 2017-04-10 14:02:30 |   900 |  30
 12 | 2017-04-10 14:02:45 |    22 |  45
 13 | 2017-04-10 14:03:00 |    34 |  60
 14 | 2017-04-10 14:03:30 |    54 |  90
(11 rows)

我添加了abs列来证明为什么我认为这些行符合要求更好

答案 1 :(得分:0)

您的问题背后存在一个仅使用SQL难以解决的优化问题。

也就是说,您的近似尝试可以实现使用索引,并且无论表大小如何都表现出良好的性能。如果你还没有这个索引,你需要这个索引:

CREATE INDEX ON points ("Userid", "Time");

查询:

SELECT *
FROM   generate_series(timestamptz '2017-04-10 14:00:00+0'
                     , timestamptz '2017-04-10 14:09:00+0'  -- 1 min *before* end!
                     , interval    '1 minute') grid(t)
LEFT  JOIN LATERAL (
   SELECT *
   FROM   points
   WHERE  "Userid" = 1
   AND    "Time" >= grid.t
   AND    "Time" <  grid.t + interval '1 minute'  -- same interval
   ORDER  BY "Time"
   LIMIT  1
   ) t ON true;

dbfiddle here

最重要的是,重写的查询可以使用上面的索引,并且 非常快 ,解决问题 B)

它还在某种程度上解决了问题 A),因为没有多次返回任何点。如果网格中的两个相邻点之间没有行,则结果中不会显示任何行。在这种情况下,使用LEFT JOIN .. ON true会保留所有网格行并附加NULL。通过切换到CROSS JOIN来消除那些NULL行。您可以通过这种方式获得更少的结果行。

我只搜索每个网格点的提前。您可以附加第二个LATERAL联接以在每个网格点后面搜索(只是另一个索引扫描),并从两个结果中取得较近的一个(忽略NULL)。但这引入了两个问题:

  • 如果一场比赛落后而下一场比赛落后,则差距会扩大。
  • 您需要对外部区间的下限和/或上限进行特殊处理
  • 您需要两个LATERAL联接,并进行两次索引扫描。

您可以使用递归CTE在实际找到的最后一次之前1分钟进行搜索,但随后返回的总行数变化更大。相关回答:

这一切都归结为你需要的确切定义,以及允许妥协的地方。

相关: