Eu estava pesquisando, autodidata e procurando solução por horas, mas sem sorte. Encontrei algumas perguntas semelhantes aqui, mas não neste caso.
Minhas mesas:
- pessoas (~ 10 milhões de linhas)
- atributos (localização, idade, ...)
- links (M: M) entre pessoas e atributos (~ 40 milhões de linhas)
Situação:
tento selecionar todos os IDs de pessoas ( person_id
) de alguns locais ( location.attribute_value BETWEEN 3000 AND 7000
), sendo de algum sexo ( gender.attribute_value = 1
), nascidos em alguns anos ( bornyear.attribute_value BETWEEN 1980 AND 2000
) e com a cor dos olhos ( eyecolor.attribute_value IN (2,3)
).
Esta é a minha consulta que levou de 3 a 4 minutos. e gostaria de otimizar:
SELECT person_id
FROM person
LEFT JOIN attribute location ON location.attribute_type_id = 1 AND location.person_id = person.person_id
LEFT JOIN attribute gender ON gender.attribute_type_id = 2 AND gender.person_id = person.person_id
LEFT JOIN attribute bornyear ON bornyear.attribute_type_id = 3 AND bornyear.person_id = person.person_id
LEFT JOIN attribute eyecolor ON eyecolor.attribute_type_id = 4 AND eyecolor.person_id = person.person_id
WHERE 1
AND location.attribute_value BETWEEN 3000 AND 7000
AND gender.attribute_value = 1
AND bornyear.attribute_value BETWEEN 1980 AND 2000
AND eyecolor.attribute_value IN (2,3)
LIMIT 100000;
Resultado:
+-----------+
| person_id |
+-----------+
| 233 |
| 605 |
| ... |
| 8702599 |
| 8703617 |
+-----------+
100000 rows in set (3 min 42.77 sec)
Explique estendido:
+----+-------------+----------+--------+---------------------------------------------+-----------------+---------+--------------------------+---------+----------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+--------+---------------------------------------------+-----------------+---------+--------------------------+---------+----------+--------------------------+
| 1 | SIMPLE | bornyear | range | attribute_type_id,attribute_value,person_id | attribute_value | 5 | NULL | 1265229 | 100.00 | Using where |
| 1 | SIMPLE | location | ref | attribute_type_id,attribute_value,person_id | person_id | 5 | test1.bornyear.person_id | 4 | 100.00 | Using where |
| 1 | SIMPLE | eyecolor | ref | attribute_type_id,attribute_value,person_id | person_id | 5 | test1.bornyear.person_id | 4 | 100.00 | Using where |
| 1 | SIMPLE | gender | ref | attribute_type_id,attribute_value,person_id | person_id | 5 | test1.eyecolor.person_id | 4 | 100.00 | Using where |
| 1 | SIMPLE | person | eq_ref | PRIMARY | PRIMARY | 4 | test1.location.person_id | 1 | 100.00 | Using where; Using index |
+----+-------------+----------+--------+---------------------------------------------+-----------------+---------+--------------------------+---------+----------+--------------------------+
5 rows in set, 1 warning (0.02 sec)
Criação de perfil:
+------------------------------+-----------+
| Status | Duration |
+------------------------------+-----------+
| Sending data | 3.069452 |
| Waiting for query cache lock | 0.000017 |
| Sending data | 2.968915 |
| Waiting for query cache lock | 0.000019 |
| Sending data | 3.042468 |
| Waiting for query cache lock | 0.000043 |
| Sending data | 3.264984 |
| Waiting for query cache lock | 0.000017 |
| Sending data | 2.823919 |
| Waiting for query cache lock | 0.000038 |
| Sending data | 2.863903 |
| Waiting for query cache lock | 0.000014 |
| Sending data | 2.971079 |
| Waiting for query cache lock | 0.000020 |
| Sending data | 3.053197 |
| Waiting for query cache lock | 0.000087 |
| Sending data | 3.099053 |
| Waiting for query cache lock | 0.000035 |
| Sending data | 3.064186 |
| Waiting for query cache lock | 0.000017 |
| Sending data | 2.939404 |
| Waiting for query cache lock | 0.000018 |
| Sending data | 3.440288 |
| Waiting for query cache lock | 0.000086 |
| Sending data | 3.115798 |
| Waiting for query cache lock | 0.000068 |
| Sending data | 3.075427 |
| Waiting for query cache lock | 0.000072 |
| Sending data | 3.658319 |
| Waiting for query cache lock | 0.000061 |
| Sending data | 3.335427 |
| Waiting for query cache lock | 0.000049 |
| Sending data | 3.319430 |
| Waiting for query cache lock | 0.000061 |
| Sending data | 3.496563 |
| Waiting for query cache lock | 0.000029 |
| Sending data | 3.017041 |
| Waiting for query cache lock | 0.000032 |
| Sending data | 3.132841 |
| Waiting for query cache lock | 0.000050 |
| Sending data | 2.901310 |
| Waiting for query cache lock | 0.000016 |
| Sending data | 3.107269 |
| Waiting for query cache lock | 0.000062 |
| Sending data | 2.937373 |
| Waiting for query cache lock | 0.000016 |
| Sending data | 3.097082 |
| Waiting for query cache lock | 0.000261 |
| Sending data | 3.026108 |
| Waiting for query cache lock | 0.000026 |
| Sending data | 3.089760 |
| Waiting for query cache lock | 0.000041 |
| Sending data | 3.012763 |
| Waiting for query cache lock | 0.000021 |
| Sending data | 3.069694 |
| Waiting for query cache lock | 0.000046 |
| Sending data | 3.591908 |
| Waiting for query cache lock | 0.000060 |
| Sending data | 3.526693 |
| Waiting for query cache lock | 0.000076 |
| Sending data | 3.772659 |
| Waiting for query cache lock | 0.000069 |
| Sending data | 3.346089 |
| Waiting for query cache lock | 0.000245 |
| Sending data | 3.300460 |
| Waiting for query cache lock | 0.000019 |
| Sending data | 3.135361 |
| Waiting for query cache lock | 0.000021 |
| Sending data | 2.909447 |
| Waiting for query cache lock | 0.000039 |
| Sending data | 3.337561 |
| Waiting for query cache lock | 0.000140 |
| Sending data | 3.138180 |
| Waiting for query cache lock | 0.000090 |
| Sending data | 3.060687 |
| Waiting for query cache lock | 0.000085 |
| Sending data | 2.938677 |
| Waiting for query cache lock | 0.000041 |
| Sending data | 2.977974 |
| Waiting for query cache lock | 0.000872 |
| Sending data | 2.918640 |
| Waiting for query cache lock | 0.000036 |
| Sending data | 2.975842 |
| Waiting for query cache lock | 0.000051 |
| Sending data | 2.918988 |
| Waiting for query cache lock | 0.000021 |
| Sending data | 2.943810 |
| Waiting for query cache lock | 0.000061 |
| Sending data | 3.330211 |
| Waiting for query cache lock | 0.000025 |
| Sending data | 3.411236 |
| Waiting for query cache lock | 0.000023 |
| Sending data | 23.339035 |
| end | 0.000807 |
| query end | 0.000023 |
| closing tables | 0.000325 |
| freeing items | 0.001217 |
| logging slow query | 0.000007 |
| logging slow query | 0.000011 |
| cleaning up | 0.000104 |
+------------------------------+-----------+
100 rows in set (0.00 sec)
Estruturas de tabelas:
CREATE TABLE `attribute` (
`attribute_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`attribute_type_id` int(11) unsigned DEFAULT NULL,
`attribute_value` int(6) DEFAULT NULL,
`person_id` int(11) unsigned DEFAULT NULL,
PRIMARY KEY (`attribute_id`),
KEY `attribute_type_id` (`attribute_type_id`),
KEY `attribute_value` (`attribute_value`),
KEY `person_id` (`person_id`)
) ENGINE=MyISAM AUTO_INCREMENT=40000001 DEFAULT CHARSET=utf8;
CREATE TABLE `person` (
`person_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`person_name` text CHARACTER SET latin1,
PRIMARY KEY (`person_id`)
) ENGINE=MyISAM AUTO_INCREMENT=20000001 DEFAULT CHARSET=utf8;
A consulta foi realizada no servidor virtual DigitalOcean com SSD e 1 GB de RAM.
Presumo que possa haver um problema com o design do banco de dados. Você tem alguma sugestão para projetar melhor esta situação, por favor? Ou apenas para ajustar a seleção acima?
attribute (person_id, attribute_type_id, attribute_value)
(attribute_type_id, attribute_value, person_id)
e(attribute_type_id, person_id, attribute_value)
Respostas:
Escolha alguns atributos para incluir
person
. Indexá-los em algumas combinações - use índices compostos, não índices de coluna única.Essa é essencialmente a única saída do EAV é péssimo desempenho, que é onde você está.
Aqui está mais discussão: http://mysql.rjweb.org/doc.php/eav, incluindo uma sugestão de usar JSON em vez da tabela de valores-chave.
fonte
Adicione indeces a
attribute
para:(person_id, attribute_type_id, attribute_value)
e(attribute_type_id, attribute_value, person_id)
Explicação
Com seu design atual,
EXPLAIN
espera que sua consulta examine as1,265,229 * 4 * 4 * 4 = 80,974,656
linhasattribute
. Você pode reduzir esse número adicionando um índice composto emattribute
para(person_id, attribute_type_id)
. Usando esse índice, sua consulta examinará apenas 1 em vez de 4 linhas para cada um delocation
,eyecolor
egender
.Você poderia estender esse índice para incluir
attribute_type_value
também:(person_id, attribute_type_id, attribute_value)
. Isso transformaria esse índice em um índice de cobertura para esta consulta, o que também deveria melhorar o desempenho.Além disso, adicionar um índice em
(attribute_type_id, attribute_value, person_id)
(novamente um índice de cobertura incluindoperson_id
) deve melhorar o desempenho, usando apenas um índice emattribute_value
que mais linhas teriam que ser examinadas. Nesse caso, ele será o primeiro passo na sua explicação: selecionando um intervalo debornyear
.O uso desses dois indeces reduziu o tempo de execução da sua consulta no meu sistema de ~ 2.0 sa ~ 0.2 s com a saída de explicação parecida com a seguinte:
fonte
SELECT person.person_id
porque, caso contrário, não seria executada, obviamente. Você fezANALYZE TABLE attribute
depois de adicionar os indeces? Você também pode adicionar sua novaEXPLAIN
saída (após adicionar indeces) à sua pergunta.Você está usando um design chamado Entidade-Atributo-Valor, que geralmente apresenta um desempenho ruim, bem, por design.
A maneira relacional clássica de projetar isso seria criar uma tabela separada para cada atributo. Em geral, você pode ter estes mesas separadas:
location
,gender
,bornyear
,eyecolor
.O seguinte depende se determinados atributos são sempre definidos para uma pessoa ou não. E, se uma pessoa pode ter apenas um valor de um atributo. Por exemplo, geralmente a pessoa tem apenas um gênero. No seu design atual, nada impede que você adicione três linhas para a mesma pessoa com valores diferentes para o gênero. Você também pode definir um valor de gênero para não 1 ou 2, mas para um número que não faça sentido, como 987, e não há restrição no banco de dados que o impeça. Mas, esse é outro problema separado da manutenção da integridade dos dados com o design do EAV.
Se você sempre conhece o sexo da pessoa, faz pouco sentido colocá-la em uma tabela separada e é muito melhor ter uma coluna não nula
GenderID
naperson
tabela, que seria uma chave estrangeira para a tabela de pesquisa com a lista de todos os sexos possíveis e seus nomes. Se você conhece o sexo da pessoa na maioria das vezes, mas nem sempre, você pode tornar esta coluna anulável e configurá-la paraNULL
quando as informações não estiverem disponíveis. Se na maioria das vezes o sexo da pessoa não é conhecido, é melhor ter uma tabela separadagender
que se vincule aperson
1: 1 e tenha linhas apenas para as pessoas que têm um sexo conhecido.Considerações semelhantes aplicam-se
eyecolor
ebornyear
- a pessoa é improvável que tenha dois valores para umeyecolor
oubornyear
.Se é possível que uma pessoa tenha vários valores para um atributo, você definitivamente o colocaria em uma tabela separada. Por exemplo, não é incomum uma pessoa ter vários endereços (casa, trabalho, correio, feriado etc.), portanto, você os lista todos em uma tabela
location
. Tabelasperson
elocation
estaria vinculado 1: M.Se estiver usando o design do EAV, faça pelo menos o seguinte.
attribute_type_id
,attribute_value
,person_id
deNOT NULL
.attribute.person_id
com linksperson.person_id
.(attribute_type_id, attribute_value, person_id)
. A ordem das colunas é importante aqui.Eu escreveria a consulta assim. Use em
INNER
vez deLEFT
junções e escreva explicitamente a subconsulta para cada atributo para fornecer ao otimizador todas as chances de usar o índice.Além disso, pode valer a pena particionar a
attribute
tabela porattribute_type_id
.fonte
JOIN ( SELECT ... )
não otimiza bem.JOINing
diretamente para a mesa funciona melhor (mas ainda é problemático).Espero ter encontrado uma solução suficiente. É inspirado neste artigo .
Resposta curta:
ft_min_word_len=1
(para MyISAM) na[mysqld]
seção einnodb_ft_min_token_size=1
(para InnoDb) nomy.cnf
arquivo, reinicie o serviço mysql.SELECT * FROM person_index WHERE MATCH(attribute_1) AGAINST("123 456 789" IN BOOLEAN MODE) LIMIT 1000
onde123
,456
a789
são IDs às quais as pessoas deveriam estar associadasattribute_1
. Esta consulta levou menos de 1 segundo.Resposta detalhada:
Etapa 1. Criando tabela com índices de texto completo. O InnoDb suporta índices de texto completo do MySQL 5.7, portanto, se você usa 5.5 ou 5.6, deve usar o MyISAM. Às vezes, é ainda mais rápido para pesquisas no FT do que o InnoDb.
Etapa 2. Insira dados da tabela EAV (entidade-atributo-valor). Por exemplo, declarado em questão, isso pode ser feito com 1 SQL simples:
O resultado deve ser algo como isto:
Etapa 3. Selecione da tabela com consulta como esta:
A consulta seleciona todas as linhas:
attr_1
:3000, 3001, 3002, 3003, 3004, 3005, 3006 or 3007
1
em que correspondeattr_2
(esta coluna representa o sexo, portanto, se essa solução foi personalizada, ela deve sersmallint(1)
com índice simples, etc ...)1980, 1981, 1982, 1983 or 1984
emattr_3
2
ou3
emattr_4
Conclusão:
Sei que esta solução não é perfeita e ideal para muitas situações, mas pode ser usada como uma boa alternativa para o design da tabela EAV.
Espero que ajude alguém.
fonte
Tente usar dicas de índice de consulta que pareçam apropriadas
Dicas do índice Mysql
fonte