限制非唯一值的返回

时间:2009-04-23 03:47:50

标签: sql mysql database

我有两张桌子。帖子和回复。将帖子视为博客条目,而回复是评论。

我想显示X个帖子,然后显示每个帖子的最新三个评论。

我的回复有一个外键“post_id”,它匹配每个帖子的“id”。

我正在尝试创建一个主页,其中包含

的内容

发布 - 答复 - 答复 --Reply

发布 --Reply

等等等等。我可以通过在我的模板中使用for循环并丢弃不需要的回复来完成此操作,但我讨厌从我不会使用的数据库中获取数据。有什么想法吗?

3 个答案:

答案 0 :(得分:1)

这实际上是一个非常有趣的问题。

HA HA DISREGARD THIS,I SUCK

在编辑时:这个答案有效,但在MySQL上,当父行数少至100时,它会变得非常慢。但是,请参阅下面的高性能修复。

显然,你可以为每个帖子运行一次这个查询:select * from comments where id = $id limit 3这会产生很多开销,因为你最终每个帖子做一个数据库查询,可怕的 N + 1个查询

如果您想一次性收到所有帖子(或者某个子集的位置),以下令人惊讶的工作。它假定注释具有单调递增的id(因为日期时间不保证是唯一的),但允许注释id在帖子之间交错。

由于auto_increment id列是单调递增的,如果comment有id,则表示你已全部设置。

首先,创建此视图。在视图中,我致电帖子parent并发表评论child

create view parent_top_3_children as
select a.*, 
(select max(id) from child where parent_id = a.id) as maxid, 
(select max(id) from child where id <  maxid 
  and parent_id = a.id) as maxidm1, 
(select max(id) from child where id < maxidm1 
  and parent_id = a.id) as maxidm2 
from parent a; 

maxidm1只是“max id减1”; maxidm2,“最大ID减2” - 即特定父ID 中的第二个和第三个最大的子ID

然后将评论加入评论中(我称之为text):

select a.*, 
b.text as latest_comment,
c.text as second_latest_comment,
d.text as third_latest_comment
from parent_top_3_children a
left outer join child b on (b.id = a.maxid)
left outer join child c on (c.id = a.maxidm1)
left outer join child d on (c.id = a.maxidm2);

当然,你可以添加你想要的任何where子句,以限制帖子:where a.category = 'foo'或其他。


这是我的表格的样子:

mysql> select * from parent;
+----+------+------+------+
| id | a    | b    | c    |
+----+------+------+------+
|  1 |    1 |    1 | NULL |
|  2 |    2 |    2 | NULL |
|  3 |    3 |    3 | NULL |
+----+------+------+------+
3 rows in set (0.00 sec)

还有一部分孩子。父母1有noo孩子:

mysql> select * from child;
+----+-----------+------+------+------+------+
| id | parent_id | a    | b    | c    | d    |
+----+-----------+------+------+------+------+

. . . .
| 18 |         3 | NULL | NULL | NULL | NULL |
| 19 |         2 | NULL | NULL | NULL | NULL |
| 20 |         2 | NULL | NULL | NULL | NULL |
| 21 |         3 | NULL | NULL | NULL | NULL |
| 22 |         2 | NULL | NULL | NULL | NULL |
| 23 |         2 | NULL | NULL | NULL | NULL |
| 24 |         3 | NULL | NULL | NULL | NULL |
| 25 |         2 | NULL | NULL | NULL | NULL |
+----+-----------+------+------+------+------+
24 rows in set (0.00 sec)

视图给了我们这个:

mysql> select * from parent_top_3;
+----+------+------+------+-------+---------+---------+
| id | a    | b    | c    | maxid | maxidm1 | maxidm2 |
+----+------+------+------+-------+---------+---------+
|  1 |    1 |    1 | NULL |  NULL |    NULL |    NULL |
|  2 |    2 |    2 | NULL |    25 |      23 |      22 |
|  3 |    3 |    3 | NULL |    24 |      21 |      18 |
+----+------+------+------+-------+---------+---------+
3 rows in set (0.21 sec)

视图的解释计划只是略显毛茸茸:

mysql> explain select * from parent_top_3;
+----+--------------------+------------+------+---------------+------+---------+------+------+-------------+
| id | select_type        | table      | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+--------------------+------------+------+---------------+------+---------+------+------+-------------+
|  1 | PRIMARY            | <derived2> | ALL  | NULL          | NULL | NULL    | NULL |    3 |             |
|  2 | DERIVED            | a          | ALL  | NULL          | NULL | NULL    | NULL |    3 |             |
|  5 | DEPENDENT SUBQUERY | child      | ALL  | PRIMARY       | NULL | NULL    | NULL |   24 | Using where |
|  4 | DEPENDENT SUBQUERY | child      | ALL  | PRIMARY       | NULL | NULL    | NULL |   24 | Using where |
|  3 | DEPENDENT SUBQUERY | child      | ALL  | NULL          | NULL | NULL    | NULL |   24 | Using where |
+----+--------------------+------------+------+---------------+------+---------+------+------+-------------+

但是,如果我们为parent_fks添加索引,它会更好:

mysql> create index pid on child(parent_id);

mysql> explain select * from parent_top_3;
+----+--------------------+------------+------+---------------+------+---------+-----------+------+-------------+
| id | select_type        | table      | type | possible_keys | key  | key_len | ref       | rows | Extra       |
+----+--------------------+------------+------+---------------+------+---------+-----------+------+-------------+
|  1 | PRIMARY            | <derived2> | ALL  | NULL          | NULL | NULL    | NULL      |    3 |             |
|  2 | DERIVED            | a          | ALL  | NULL          | NULL | NULL    | NULL      |    3 |             |
|  5 | DEPENDENT SUBQUERY | child      | ref  | PRIMARY,pid   | pid  | 5       | util.a.id |    2 | Using where |
|  4 | DEPENDENT SUBQUERY | child      | ref  | PRIMARY,pid   | pid  | 5       | util.a.id |    2 | Using where |
|  3 | DEPENDENT SUBQUERY | child      | ref  | pid           | pid  | 5       | util.a.id |    2 | Using where |
+----+--------------------+------------+------+---------------+------+---------+-----------+------+-------------+
5 rows in set (0.04 sec)

如上所述,当父行的数量很少为100时,即使我们使用主键索引到父,这也会开始分崩离析:

mysql> select * from parent_top_3 where  id < 10;
+----+------+------+------+-------+---------+---------+
| id | a    | b    | c    | maxid | maxidm1 | maxidm2 |
+----+------+------+------+-------+---------+---------+
|  1 |    1 |    1 | NULL |  NULL |    NULL |    NULL |
|  2 |    2 |    2 | NULL |    25 |      23 |      22 |
|  3 |    3 |    3 | NULL |    24 |      21 |      18 |
|  4 | NULL |    1 | NULL |    65 |      64 |      63 |
|  5 | NULL |    2 | NULL |    73 |      72 |      71 |
|  6 | NULL |    3 | NULL |   113 |     112 |     111 |
|  7 | NULL |    1 | NULL |   209 |     208 |     207 |
|  8 | NULL |    2 | NULL |   401 |     400 |     399 |
|  9 | NULL |    3 | NULL |   785 |     784 |     783 |
+----+------+------+------+-------+---------+---------+
9 rows in set (1 min 3.11 sec)

(请注意,我故意在慢速机器上测试,数据保存在慢速闪存盘上。)

这是解释,正好在寻找一个id(以及第一个id):

mysql> explain select * from parent_top_3 where id = 1;
+----+--------------------+------------+------+---------------+------+---------+-----------+------+-------------+
| id | select_type        | table      | type | possible_keys | key  | key_len | ref       | rows | Extra       |
+----+--------------------+------------+------+---------------+------+---------+-----------+------+-------------+
|  1 | PRIMARY            | <derived2> | ALL  | NULL          | NULL | NULL    | NULL      | 1000 | Using where |
|  2 | DERIVED            | a          | ALL  | NULL          | NULL | NULL    | NULL      | 1000 |             |
|  5 | DEPENDENT SUBQUERY | child      | ref  | PRIMARY,pid   | pid  | 5       | util.a.id |  179 | Using where |
|  4 | DEPENDENT SUBQUERY | child      | ref  | PRIMARY,pid   | pid  | 5       | util.a.id |  179 | Using where |
|  3 | DEPENDENT SUBQUERY | child      | ref  | pid           | pid  | 5       | util.a.id |  179 | Using where |
 +----+--------------------+------------+------+---------------+------+---------+-----------+------+-------------+
 5 rows in set (56.01 sec)

即使在我的慢速机器上,一排超过56秒也是不可接受的两个数量级。

那么我们可以保存这个查询吗? 工作,它太慢了。

这是修改后的查询的解释计划。它看起来很糟糕或更糟:

mysql> explain select * from parent_top_3a where id = 1;
+----+--------------------+------------+------+---------------+------+---------+-----------+------+-------------+
| id | select_type        | table      | type | possible_keys | key  | key_len | ref       | rows | Extra       |
+----+--------------------+------------+------+---------------+------+---------+-----------+------+-------------+
|  1 | PRIMARY            | <derived2> | ALL  | NULL          | NULL | NULL    | NULL      |  100 | Using where |
|  2 | DERIVED            | <derived4> | ALL  | NULL          | NULL | NULL    | NULL      |  100 |             |
|  4 | DERIVED            | <derived6> | ALL  | NULL          | NULL | NULL    | NULL      |  100 |             |
|  6 | DERIVED            | a          | ALL  | NULL          | NULL | NULL    | NULL      |  100 |             |
|  7 | DEPENDENT SUBQUERY | child      | ref  | pid           | pid  | 5       | util.a.id |  179 | Using where |
|  5 | DEPENDENT SUBQUERY | child      | ref  | PRIMARY,pid   | pid  | 5       | a.id      |  179 | Using where |
|  3 | DEPENDENT SUBQUERY | child      | ref  | PRIMARY,pid   | pid  | 5       | a.id      |  179 | Using where |
+----+--------------------+------------+------+---------------+------+---------+-----------+------+-------------+
7 rows in set (0.05 sec)

但它在1/20秒内完成三个的数量级更快!

我们如何获得更快的parent_top_3a?我们创建三个视图,每个视图都依赖于前一个视图:

create view parent_top_1 as  
select a.*, 
(select max(id) from child where parent_id = a.id) 
 as maxid 
from parent a;

create view parent_top_2 as  
select a.*, 
(select max(id) from child where parent_id = a.id and id < a.maxid) 
 as maxidm1 
from parent_top_1 a;

create view parent_top_3a as  
select a.*, 
(select max(id) from child where parent_id = a.id and id < a.maxidm1)
 as maxidm2 
from parent_top_2 a;

这不仅可以更快地完成,而且对于除MySQL以外的RDBMS也是合法的。

让我们将父行数增加到12800,将子行数增加到1536(大多数博客帖子都没有得到评论,对吗?))

mysql> select * from parent_top_3a where id >= 20 and id < 40;
+----+------+------+------+-------+---------+---------+
| id | a    | b    | c    | maxid | maxidm1 | maxidm2 |
+----+------+------+------+-------+---------+---------+
| 39 | NULL |    2 | NULL |  NULL |    NULL |    NULL |
| 38 | NULL |    1 | NULL |  NULL |    NULL |    NULL |
| 37 | NULL |    3 | NULL |  NULL |    NULL |    NULL |
| 36 | NULL |    2 | NULL |  NULL |    NULL |    NULL |
| 35 | NULL |    1 | NULL |  NULL |    NULL |    NULL |
| 34 | NULL |    3 | NULL |  NULL |    NULL |    NULL |
| 33 | NULL |    2 | NULL |  NULL |    NULL |    NULL |
| 32 | NULL |    1 | NULL |  NULL |    NULL |    NULL |
| 31 | NULL |    3 | NULL |  NULL |    NULL |    NULL |
| 30 | NULL |    2 | NULL |  1537 |    1536 |    1535 |
| 29 | NULL |    1 | NULL |  1529 |    1528 |    1527 |
| 28 | NULL |    3 | NULL |  1513 |    1512 |    1511 |
| 27 | NULL |    2 | NULL |  1505 |    1504 |    1503 |
| 26 | NULL |    1 | NULL |  1481 |    1480 |    1479 |
| 25 | NULL |    3 | NULL |  1457 |    1456 |    1455 |
| 24 | NULL |    2 | NULL |  1425 |    1424 |    1423 |
| 23 | NULL |    1 | NULL |  1377 |    1376 |    1375 |
| 22 | NULL |    3 | NULL |  1329 |    1328 |    1327 |
| 21 | NULL |    2 | NULL |  1281 |    1280 |    1279 |
| 20 | NULL |    1 | NULL |  1225 |    1224 |    1223 |
+----+------+------+------+-------+---------+---------+
20 rows in set (1.01 sec)

请注意,这些时间适用于MyIsam表;我会留给其他人在Innodb上做时间。


但是使用Postgresql,在类似但不相同的数据集上,我们在涉及where列的parent谓词上得到了类似的时间:

 postgres=# select (select count(*) from parent) as parent_count, (select count(*) 
from child) as child_count;
 parent_count | child_count
--------------+-------------
        12289 |        1536

postgres=# select * from parent_top_3a where id >= 20 and id < 40;
 id | a | b  | c | maxid | maxidm1 | maxidm2
----+---+----+---+-------+---------+---------
 20 |   | 18 |   |  1464 |    1462 |    1461
 21 |   | 88 |   |  1463 |    1460 |    1457
 22 |   | 72 |   |  1488 |    1486 |    1485
 23 |   | 13 |   |  1512 |    1510 |    1509
 24 |   | 49 |   |  1560 |    1558 |    1557
 25 |   | 92 |   |  1559 |    1556 |    1553
 26 |   | 45 |   |  1584 |    1582 |    1581
 27 |   | 37 |   |  1608 |    1606 |    1605
 28 |   | 96 |   |  1607 |    1604 |    1601
 29 |   | 90 |   |  1632 |    1630 |    1629
 30 |   | 53 |   |  1631 |    1628 |    1625
 31 |   | 57 |   |       |         |
 32 |   | 64 |   |       |         |
 33 |   | 79 |   |       |         |
 34 |   | 37 |   |       |         |
 35 |   | 60 |   |       |         |
 36 |   | 75 |   |       |         |
 37 |   | 34 |   |       |         |
 38 |   | 87 |   |       |         |
 39 |   | 43 |   |       |         |
(20 rows)

Time: 91.139 ms

答案 1 :(得分:0)

听起来你只想要一个LIMIT语句的SELECT子句:

SELECT comment_text, other_stuff FROM comments WHERE post_id = POSTID ORDER BY comment_time DESC LIMIT 3;

您需要为每个要显示评论的帖子运行此查询一次。有一些方法可以解决这个问题,如果你愿意在追求极致表现的过程中牺牲可维护性和理智:

  1. 如上所述,每个帖子都有一个查询来检索评论。很简单,但可能不是那么快。

  2. 检索您要显示评论的post_ids列表,然后检索这些帖子的所有评论,并在客户端过滤它们(或者如果您有窗口,则可以在服务器端执行此操作我认为,虽然那些不在MySQL中。在服务器端很简单,但是客户端过滤会很难看,而且你仍然会将大量数据从服务器移动到客户端,所以这可能也不会那么快。

  3. 作为#1,但使用与您要显示的帖子一样多的查询的{5}},因此您运行的是一个令人讨厌的查询,而不是N个小查询。很丑,但它会比选项1或2更快。你仍然需要对客户端进行一些过滤,但仔细编写UNION ALL会比#所需的过滤更容易2,没有浪费的数据将通过电线发送。不过,它会产生一个丑陋的查询。

  4. 加入帖子和评论表,部分轮播评论。如果您只需要一个评论,这是非常干净的,但如果你想要三个,它会很快变得混乱。在客户端方面很棒,但是比#3更糟糕的SQL,并且可能更难以启动服务器。

  5. 在一天结束时,我会选择上面的简单查询选项1,而不用担心每个帖子执行一次的开销。如果你只需要一个评论,那么加入选项可能是可以接受的,但你需要三个并且规则它。如果窗口函数被添加到MySQL(它们在PostgreSQL的8.4版本中),则选项2可能变得可口或甚至更可取。但直到那天,只需选择简单易懂的查询。

答案 2 :(得分:0)

虽然可能有一种聪明的方法可以在一个没有架构更改的查询中得到它,但我猜它无论如何都不会是高性能编辑:看起来tpdi有一个聪明的解决方案。它看起来可能非常快,但我很想看到特定数据库的基准测试。

鉴于高性能和最小数据传输的限制,我有两个建议。

没有架构更改或维护的解决方案

首先:

SELECT * FROM Posts

收集ids,然后:

SELECT id FROM Replies WHERE post_id IN (?) ORDER BY id DESC

最后,遍历这些id,只抓取每个post_id的前3个,然后执行:

SELECT * FROM Replies WHERE post_id IN (?)

如果您愿意维护一些缓存列

,则更有效的解决方案

第二个解决方案是假设读取的次数远远多于写入次数,您可以通过在每次添加回复时将最后三个注释ID存储在Posts表上来最小化查找。在这种情况下,您只需添加三列last_reply_idsecond_reply_idthird_reply_id或其他类似列。然后,您可以使用以下两个查询进行查询:

SELECT * FROM Posts

从这些字段中收集ID,然后:

SELECT * FROM Replies WHERE post_id IN (?)

如果您有这些字段,您还可以手动构建三重连接,这将在一个查询中获取数据,尽管字段列表非常详细。像

这样的东西
SELECT posts.*, r1.title, r2.title ... FROM Posts 
  LEFT JOIN Replies as r1 ON Posts.last_reply_id = Replies.id
  LEFT JOIN Replies as r2 ON Posts.second_reply_id = Replies.id
  ...

您更喜欢哪种可能取决于您的ORM或语言。