【问题标题】:limit 1 in joined table?连接表中的限制为 1?
【发布时间】:2019-03-06 19:34:48
【问题描述】:

表格

  • ip2country 有 250k 行(通过 from_ip 升序插入的 ip 范围)
  • sessions 有 50 行

明显而缓慢(2.687 秒):

SELECT
  s.*,
  ip.country
FROM
  sessions s
  JOIN ip2country ip ON s.ip_addr BETWEEN ip.from_ip AND ip.to_ip

虽然这本身很快(0.031 秒):

SELECT
  *
FROM
  ip2country
WHERE
  from_ip >= 387703808
LIMIT 1

所以基本上问题归结为能够在连接表中使用 LIMIT。可以做到这一点,它会是什么样子? (MySQL 5.7.24)

【问题讨论】:

  • 你为什么要这样做?
  • LIMIT 在表扫描后完成。它不会有任何实际效果。您的表中是否设置了正确的索引?尝试在第一条语句上使用 EXPLAIN
  • 是的,索引在那里,但它们根本没有帮助。解释看起来不太好:( 1 SIMPLE s ALL ip_addr 48 100.00 1 SIMPLE ip ALL to_ip,from_ip 248160 11.11 检查每条记录的范围(索引图:0x6)
  • 嗯,这看起来不太好:stackoverflow.com/questions/8195304/…
  • 非常糟糕:mariadb.com/kb/en/library/ip-range-table-performance。看起来我坚持在插入过程中使用来自 ip2country 的 PK 在会话表中添加“geo”列和标记记录。确实是一个悲伤但快速且最不复杂的“解决方案”。

标签: mysql join limit


【解决方案1】:

这是一个类似的例子:

我有一个包含 100 个 IP(32 位整数)的表和一个包含 1M 个 IP 范围的表。 (请参阅下面的架构和示例数据。)

以下查询与您的类似:

select *
from ips i join ip_ranges r
  on i.ip between r.ip_from and r.ip_to

返回100个对应范围的IP需要9.6秒。每个 IP 为 100 毫秒。如果我只搜索一个IP

select *
from ip_ranges r
where 555555555 between ip_from and ip_to

大约需要 100 毫秒(如预期的那样)。请注意,对于 IP = 1,我将在“零”时间得到结果,但对于 IP = 999,999,999,我将等待 200 毫秒。所以 100 毫秒是平均值。

添加LIMIT 1 在这里没有帮助。但结合ORDER BY ip_from DESC,我会在“零时间”内得到结果。

现在我可以尝试在子查询中为每个 IP 运行一个 LIMIT 1

select i.ip
, (
    select ip_from
    from ip_ranges r
    where i.ip between r.ip_from and r.ip_to
    order by r.ip_from desc
    limit 1
) as ip_from
from ips i

但是 MySQL(在我的例子中是 5.6)在这里做得很差,执行需要 13 秒。

所以我们所能做的就是获取所有 IP 并为每个 IP 执行一个查询。这至少会快于 10 秒。

另一种方法是生成一个 UNION ALL 查询,每个 IP 有一个子查询。您可以在您的应用程序中执行此操作,也可以使用动态预准备语句直接在 SQL 中执行此操作:

set @subquery = '(
    select {ip} as ip, r.*
    from ip_ranges r
    where {ip} between ip_from and ip_to
    order by ip_from desc
    limit 1
)';

set session group_concat_max_len = 1000000000;

set @sql = (select group_concat(replace(@subquery, '{ip}', ip) separator 'union all') from ips);

prepare stmt from @sql;
execute stmt;

此查询在不到 1 毫秒内执行。

测试架构和数据

create table ips(
    ip int unsigned primary key
);

insert into ips(ip)
    select floor(rand(1) * pow(10, 9))
    from seq1m s
    limit 100
;


create table ip_ranges(
    ip_from int unsigned not null,
    ip_to   int unsigned not null,
    primary key (ip_from, ip_to)
);

insert into ip_ranges
    select (s.seq - 1) * 1000 as ip_from
         , s.seq * 1000 - 1   as ip_to
    from seq1m s
    limit 1000000
;

seq1m 是一个有 1M 序列号的表。你可以创建它

create table seq1m (seq int auto_increment primary key);
insert into seq1m (seq)
    select null
    from information_schema.COLUMNS a
       , information_schema.COLUMNS b
    limit 1000000;

【讨论】:

  • 感谢您的详尽回答。我决定在会话初始化/插入的同时简单地用 IP 范围的 PK 标记记录。它使代码更简单、更明显,并且基本上是免费的。 INSERT INTO sessions (key, ip_addr, ip_range_id) SELECT 'abc123', '128.55.64.25', id FROM ip_ranges r WHERE inet_aton('128.55.64.25') <= r.max LIMIT 1(ip_ranges 已经是自然排序的 ASC)。
  • 您的解决方案看起来不错。我猜/希望你有一个以r.max 开头的索引。虽然我会添加ORDER BY r.max ASC - 所以你不要中继实现细节。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-01-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多