我需要一个更快的解决方案来在 table 上执行 Select,在 1 对 n 相关 table 中具有多个特定匹配

I need a faster solution to perform a Select on a table with multiple specific matchings in a 1-to-n related table

概述

我在系统中有以下结构,用户可以在其中保存作业的属性,他可以自己命名。一个属性可以有 2 个可能的值,一个用户值和一个系统值。 我正在尝试根据属性过滤作业,用户值或系统值都可以命中。

现状

table 职位

id company_id status
1 2 active
2 2 created
3 3 created
4 12 inactive

table job_data

job_id field value_user value_xml
1 city Berlin
1 phone 1234567
1 type fulltime
2 city New York
2 phone 33333333
2 type parttime
3 city Berlin
3 phone 123
3 type fulltime

索引:

Table Key_name Column_name Collation
jobs PRIMARY id A
job_data job_data_jobs_id_foreign job_id A

用户现在必须过滤多个属性,例如:“向我显示城市柏林和 phone 123 的所有工作”。这将显示工作 1 和 3,因为它们都在其中一个城市值中有柏林,并且在 phone 值之一中有一个 phone 数字,如 123。

我有几个可行的解决方案,但现在我们的数据库中有 120,000 个活动作业,具有超过一百万个属性,而且我们的代码速度不够快。这是我们目前的解决方案:

SELECT * FROM jobs
WHERE
(
    SELECT count(*) FROM job_data
    WHERE job_data.job_id = jobs.id
    AND job_data.field = "city" AND (job_data.value_xml LIKE "%Martinhaven%" OR job_data.value_user LIKE "%Martinhaven%")
) > 0
AND
(
    SELECT count(*) FROM job_data
    WHERE job_data.jobposting_id = jobs.id
    AND job_data.field = "category" AND (job_data.value_xml LIKE "%omnis%" OR job_data.value_user LIKE "%omnis%")
) > 0;

输出:

id company_id status
1 2 active
3 3 created

这已被简化,我们有大约 6 种过滤器可能性,但它们都是同一类,所以我只发布了其中的 2 种。

问题

谁能告诉我,我怎样才能更快地做到这一点?我们目前需要 5-10 秒来完成一个 select。 Ofc 我们可以重组数据库以使其过滤速度更快,但我们仍在努力防止这种情况发生,因为它是一个更大的系统正在建设中。

非常感谢。

不使用 1 个针对每个条件和 table jobs 的每一行进行聚合的相关子查询,而是使用一次条件聚合:

SELECT job_id 
FROM job_data
GROUP BY job_id
HAVING SUM(field = 'city' AND (value_xml LIKE '%Martinhaven%' OR value_user LIKE '%Martinhaven%')) > 0
   AND SUM(field = 'category' AND (value_xml LIKE '%omnis%' OR value_user LIKE '%omnis%')) > 0
   AND .....

获取您想要的所有 job_id 并将它们与运算符 IN:

一起使用
SELECT * FROM jobs
WHERE job_id IN (
  SELECT job_id 
  FROM job_data
  GROUP BY job_id
  HAVING SUM(field = 'city' AND (value_xml LIKE '%Martinhaven%' OR value_user 
  LIKE '%Martinhaven%')) > 0
     AND SUM(field = 'category' AND (value_xml LIKE '%omnis%' OR value_user 
  LIKE '%omnis%')) > 0
     AND .....
);

如果子查询返回的job_id个数小我相信这个查询的性能会更好

可以从代码中删除所有 > 0 不等式,但为了清楚起见,我将它们留在那里。

你有两个问题。

  • 多个值字段。
  • like '%word%' 很难索引。

tl;dr demonstration


修复架构

job_data 更改为单个值和来源。

job_id  field  value   source
1       city   Berlin  user
3       city   Berlin  xml
-- Add value and source columns, nullable for now.
alter table job_data add value varchar(255), add source varchar(255);

-- If value_user is not null or blank, add it to value with a source of user.
update job_data
set value = value_user, source = 'user'
where coalesce(value_user, '') <> '';

-- Same for value_xml, source of xml.
update job_data
set value = value_xml, source = 'xml'
where value_xml is not null;

-- Drop the old value columns.
alter table job_data
  drop value_xml, drop value_user;

现在我们可以对值和源强制执行 not null 以避免错误数据。

alter table job_data
  modify value varchar(255) not null,
  modify source varchar(255) not null;

我们还可以强制一个作业的每个字段只能有一个值。或按字段和来源。

-- If a job can have multiple sources for a field.
alter table add unique(job_id, field, source);

-- If it cannot.
alter table add unique(job_id, field);

如果您需要保持兼容性,请提出意见。

create view old_job_data as
  select
    job_id,
    field,
    case source when 'user' then value end as value_user,
    case source when 'xml' then value end as value_xml
  from job_data;

这解决了一些问题。

  • 您可以拥有任意数量的来源。
  • 您可以确保该值不为空。
  • 您可以避免重复。
  • 您不需要检查两个地方的值。
  • 索引更容易。

查询

有了这个固定,查询就简单多了。

要查找哪些工作匹配两个 field/value 对,通常您会执行 intersect。但是MySQL没有intersect,所以我们用自连接来模拟。

select * from jobs
where job_id in (
  select distinct j1.job_id
  from job_data j1
  inner join job_data j2 on j1.job_id = j2.job_id
  where
    (j1.field = 'city' and j1.value like '%York%')
    and
    (j2.field = 'type' and j2.value like '%time%')

)

没有 来改变 table,你可以 j1.field = 'city' and (j1.value_xml like '%York%' or j1.value_user like '%York%'),但这样更容易。


索引

在MySQL、like '%word%' will not use a simple index. It has to scan each row with a matching field. Simple indexes only work for like 'word%'. Other databases have special indexes for this。我不知道 MySQL.

的解决方案

至少索引 field 这样您至少可以快速找到匹配 field 的行。

create index job_data_field_idx on job_data(field);

现在不再扫描每一行,它只会扫描与该字段匹配的行。

确定您是否真的需要完整的通配符搜索。或者您是否需要在插入数据之前清理数据?如果你能摆脱通配符索引,这个索引将使搜索几乎是即时的。

create index job_data_field_vaue_idx on job_data(value, field);

当使用带有 like '%...' 的不可压缩标准时,这没有太多帮助,但是你不必要地燃烧 CPU 和 IO对每个搜索条件执行 count(*)

可能优化器可以发现这一点并在内部将其优化为存在操作,但最好是显式

select * 
from jobs j
where exists (
    select 1 job_data d
    where d.job_id = j.id
    and d.field = "city" and (d.value_xml like "%Martinhaven%" or d.value_user like "%Martinhaven%")
) 

对于实体属性模型,如果所有值都在同一列中,效率会更高。最初实施的更改可能太大,因此您还应该尝试使用多个条件分别检查每一列,而不是使用 。这对于每个索引(在 字段,Value_xml 字段,value_user)是最有益的。 =13=]

select * 
from jobs j
where exists (select 1 job_data d  where d.job_id = j.id and d.field = "city" and d.value_xml like "%Martinhaven%") 
union
select * 
from jobs j
where exists (select 1 job_data d  where d.job_id = j.id and d.field = "city" and d.value_user like "%Martinhaven%") 

您可能会发现这会产生更好的性能,因为每个索引都可以使用索引来查找 city 行,然后扫描直到找到匹配项;充其量它几乎可以立即存在,最坏的情况下它并不比现有的 table 扫描差。

部分解:

而不是

( SELECT COUNT(*) ... ) > 0 )

写入

EXISTS ( SELECT 1 ... )

后者会在第一次出现时停止扫描。 (前者要看整个table。)

真正的反派是

  • OR -- 优化不好
  • LIKE '%...' -- 'leading wildcard' 阻止使用任何索引。

请提供 SHOW CREATE TABLE 以便我们查看存在哪些索引。

另一个问题是笨拙且低效的“实体-属性-值”模式模式的使用。

我担心“XML”。

现在来说说更鼓舞人心的事情。在包含要搜索的数据的列上设置 FULLEXT,再加上 MATCH(col) AGAINST('+Martinhaven +omnis' IN BOOLEAN MODE) 会非常快。