Como otimizar SELECT muito lento com JOFT ESQUERDA em grandes tabelas

14

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)

Despejo completo ~ 280MB

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?

Martin
fonte
4
Esse é o preço que você paga pelo design do EAV. Você pode tentar um índice composto emattribute (person_id, attribute_type_id, attribute_value)
mustaccio
1
Eu tentaria adicionar estes índices: (attribute_type_id, attribute_value, person_id)e (attribute_type_id, person_id, attribute_value)
ypercubeᵀᴹ #
5
E use o InnoDB, jogue fora o MyISAM. Estamos em 2015, o MyiSAM está morto há muito tempo.
ypercubeᵀᴹ
2
Primeira coisa: livrar-se da junção ESQUERDA, ela não tem efeito, pois você usa todas as tabelas em sua condição WHERE, transformando efetivamente todas as junções em junções INNER (o otimizador deve entender e otimizar isso, mas é melhor não dificultar ) Segunda coisa - desativar o cache de consulta, a menos que você tem uma forte razão para usá-lo (= você testou-lo e medir que ele ajuda você)
jkavalik
2
OT: não é estranho o uso de LIMIT em nosso pedido? Isso retornará algumas 100000 linhas aleatórias?
Ola5041

Respostas:

6

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.

Rick James
fonte
3

Adicione indeces a attributepara:

  • (person_id, attribute_type_id, attribute_value) e
  • (attribute_type_id, attribute_value, person_id)

Explicação

Com seu design atual, EXPLAINespera que sua consulta examine as 1,265,229 * 4 * 4 * 4 = 80,974,656linhas attribute. Você pode reduzir esse número adicionando um índice composto em attributepara (person_id, attribute_type_id). Usando esse índice, sua consulta examinará apenas 1 em vez de 4 linhas para cada um de location, eyecolore gender.

Você poderia estender esse índice para incluir attribute_type_valuetambé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 incluindo person_id) deve melhorar o desempenho, usando apenas um índice em attribute_valueque mais linhas teriam que ser examinadas. Nesse caso, ele será o primeiro passo na sua explicação: selecionando um intervalo de bornyear.

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:

+----+-------------+----------+--------+-------------------------------------+-------------------+---------+--------------------------------+---------+----------+--------------------------+
| id | select_type | table    | type   | possible_keys                       | key               | key_len | ref                            |    rows | filtered | Extra                    |
+----+-------------+----------+--------+-------------------------------------+-------------------+---------+--------------------------------+---------+----------+--------------------------+
|  1 | SIMPLE      | bornyear | range  | person_type_value,type_value_person | type_value_person |       9 |                                | 1861881 |   100.00 | Using where; Using index |
|  1 | SIMPLE      | location | ref    | person_type_value,type_value_person | person_type_value |       8 | bornyear.person_id,const       |       1 |   100.00 | Using where; Using index |
|  1 | SIMPLE      | eyecolor | ref    | person_type_value,type_value_person | person_type_value |       8 | bornyear.person_id,const       |       1 |   100.00 | Using where; Using index |
|  1 | SIMPLE      | gender   | ref    | person_type_value,type_value_person | person_type_value |      13 | bornyear.person_id,const,const |       1 |   100.00 | Using index              |
|  1 | SIMPLE      | person   | eq_ref | PRIMARY                             | PRIMARY           |       4 | bornyear.person_id             |       1 |   100.00 | Using index              |
+----+-------------+----------+--------+-------------------------------------+-------------------+---------+--------------------------------+---------+----------+--------------------------+
wolfgangwalther
fonte
1
Obrigado pela extensa resposta e explicação. Fiz tudo o que você mencionou, mas a consulta ainda leva ~ 2 minutos. Por favor, que tipo de tabela (innodb, myisam) você está usando e qual consulta exata foi realizada?
Martin
1
Além de adicionar os indeces, usei exatamente os mesmos dados e definições que você, por isso usei o MyISAM. Alterei a primeira linha da sua consulta para SELECT person.person_idporque, caso contrário, não seria executada, obviamente. Você fez ANALYZE TABLE attributedepois de adicionar os indeces? Você também pode adicionar sua nova EXPLAINsaída (após adicionar indeces) à sua pergunta.
wolfgangwalther
3

Presumo que possa haver um problema com o design do banco de dados.

Você está usando um design chamado Entidade-Atributo-Valor, que geralmente apresenta um desempenho ruim, bem, por design.

Você tem alguma sugestão para projetar melhor esta situação, por favor?

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 GenderIDna persontabela, 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 para NULLquando as informações não estiverem disponíveis. Se na maioria das vezes o sexo da pessoa não é conhecido, é melhor ter uma tabela separada genderque se vincule a person1: 1 e tenha linhas apenas para as pessoas que têm um sexo conhecido.

Considerações semelhantes aplicam-se eyecolore bornyear- a pessoa é improvável que tenha dois valores para um eyecolorou bornyear.

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. Tabelas persone locationestaria vinculado 1: M.


Ou apenas para ajustar a seleção acima?

Se estiver usando o design do EAV, faça pelo menos o seguinte.

  • Definir colunas attribute_type_id, attribute_value, person_idde NOT NULL.
  • Configure uma chave estrangeira attribute.person_idcom links person.person_id.
  • Crie um índice em três colunas (attribute_type_id, attribute_value, person_id). A ordem das colunas é importante aqui.
  • Até onde eu sei, o MyISAM não respeita chaves estrangeiras; portanto, não use-o, use o InnoDB.

Eu escreveria a consulta assim. Use em INNERvez de LEFTjunções e escreva explicitamente a subconsulta para cada atributo para fornecer ao otimizador todas as chances de usar o índice.

SELECT person.person_id
FROM
    person
    INNER JOIN
    (
        SELECT attribute.person_id
        FROM attribute
        WHERE attribute_type_id = 1
            AND location.attribute_value BETWEEN 3000 AND 7000
    ) AS location ON location.person_id = person.person_id
    INNER JOIN
    (
        SELECT attribute.person_id
        FROM attribute
        WHERE attribute_type_id = 2
            AND location.attribute_value = 1
    ) AS gender ON gender.person_id = person.person_id
    INNER JOIN
    (
        SELECT attribute.person_id
        FROM attribute
        WHERE attribute_type_id = 3
            AND location.attribute_value BETWEEN 1980 AND 2000
    ) AS bornyear ON bornyear.person_id = person.person_id
    INNER JOIN
    (
        SELECT attribute.person_id
        FROM attribute
        WHERE attribute_type_id = 4
            AND location.attribute_value IN (2, 3)
    ) AS eyecolor ON eyecolor.person_id = person.person_id
LIMIT 100000;

Além disso, pode valer a pena particionar a attributetabela por attribute_type_id.

Vladimir Baranov
fonte
Cuidado com o desempenho: JOIN ( SELECT ... )não otimiza bem. JOINingdiretamente para a mesa funciona melhor (mas ainda é problemático).
Rick James
2

Espero ter encontrado uma solução suficiente. É inspirado neste artigo .

Resposta curta:

  1. Eu criei 1 tabela com todos os atributos. Uma coluna para um atributo. Além disso, coluna de chave primária.
  2. Os valores dos atributos são armazenados nas células de texto (para pesquisa em texto completo) no formato CSV.
  3. Criamos índices de texto completo. Antes disso, é importante definir ft_min_word_len=1(para MyISAM) na [mysqld]seção e innodb_ft_min_token_size=1(para InnoDb) no my.cnfarquivo, reinicie o serviço mysql.
  4. Pesquisando exemplo: SELECT * FROM person_index WHERE MATCH(attribute_1) AGAINST("123 456 789" IN BOOLEAN MODE) LIMIT 1000onde 123, 456a 789são IDs às quais as pessoas deveriam estar associadas attribute_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.

CREATE TABLE `person_attribute_ft` (
  `person_id` int(11) NOT NULL,
  `attr_1` text,
  `attr_2` text,
  `attr_3` text,
  `attr_4` text,
  PRIMARY KEY (`person_id`),
  FULLTEXT KEY `attr_1` (`attr_1`),
  FULLTEXT KEY `attr_2` (`attr_2`),
  FULLTEXT KEY `attr_3` (`attr_3`),
  FULLTEXT KEY `attr_4` (`attr_4`),
  FULLTEXT KEY `attr_12` (`attr_1`,`attr_2`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8

Etapa 2. Insira dados da tabela EAV (entidade-atributo-valor). Por exemplo, declarado em questão, isso pode ser feito com 1 SQL simples:

INSERT IGNORE INTO `person_attribute_ft`
SELECT
    p.person_id,
    (SELECT GROUP_CONCAT(a.attribute_value SEPARATOR ' ') FROM attribute a WHERE a.attribute_type_id = 1 AND a.person_id = p.person_id LIMIT 10) attr_1,
    (SELECT GROUP_CONCAT(a.attribute_value SEPARATOR ' ') FROM attribute a WHERE a.attribute_type_id = 2 AND a.person_id = p.person_id LIMIT 10) attr_2,
    (SELECT GROUP_CONCAT(a.attribute_value SEPARATOR ' ') FROM attribute a WHERE a.attribute_type_id = 3 AND a.person_id = p.person_id LIMIT 10) attr_3,
    (SELECT GROUP_CONCAT(a.attribute_value SEPARATOR ' ') FROM attribute a WHERE a.attribute_type_id = 4 AND a.person_id = p.person_id LIMIT 10) attr_4
FROM person p

O resultado deve ser algo como isto:

mysql> select * from person_attribute_ft limit 10;
+-----------+--------+--------+--------+--------+
| person_id | attr_1 | attr_2 | attr_3 | attr_4 |
+-----------+--------+--------+--------+--------+
|         1 | 541    | 2      | 1927   | 3      |
|         2 | 2862   | 2      | 1939   | 4      |
|         3 | 6573   | 2      | 1904   | 2      |
|         4 | 2432   | 1      | 2005   | 2      |
|         5 | 2208   | 1      | 1995   | 4      |
|         6 | 8388   | 2      | 1973   | 1      |
|         7 | 107    | 2      | 1909   | 4      |
|         8 | 5161   | 1      | 2005   | 1      |
|         9 | 8022   | 2      | 1953   | 4      |
|        10 | 4801   | 2      | 1900   | 3      |
+-----------+--------+--------+--------+--------+
10 rows in set (0.00 sec)

Etapa 3. Selecione da tabela com consulta como esta:

mysql> SELECT SQL_NO_CACHE *
    -> FROM `person_attribute_ft`
    -> WHERE 1 AND MATCH(attr_1) AGAINST ("3000 3001 3002 3003 3004 3005 3006 3007" IN BOOLEAN MODE)
    -> AND MATCH(attr_2) AGAINST ("1" IN BOOLEAN MODE)
    -> AND MATCH(attr_3) AGAINST ("1980 1981 1982 1983 1984" IN BOOLEAN MODE)
    -> AND MATCH(attr_4) AGAINST ("2,3" IN BOOLEAN MODE)
    -> LIMIT 10000;
+-----------+--------+--------+--------+--------+
| person_id | attr_1 | attr_2 | attr_3 | attr_4 |
+-----------+--------+--------+--------+--------+
|     12131 | 3002   | 1      | 1982   | 2      |
|     51315 | 3007   | 1      | 1984   | 2      |
|    147283 | 3001   | 1      | 1984   | 2      |
|    350086 | 3005   | 1      | 1982   | 3      |
|    423907 | 3004   | 1      | 1982   | 3      |
... many rows ...
|   9423907 | 3004   | 1      | 1982   | 3      |
|   9461892 | 3007   | 1      | 1982   | 2      |
|   9516361 | 3006   | 1      | 1980   | 2      |
|   9813933 | 3005   | 1      | 1982   | 2      |
|   9986892 | 3003   | 1      | 1981   | 2      |
+-----------+--------+--------+--------+--------+
90 rows in set (0.17 sec)

A consulta seleciona todas as linhas:

  • correspondendo a pelo menos um desses códigos em attr_1:3000, 3001, 3002, 3003, 3004, 3005, 3006 or 3007
  • E ao mesmo tempo 1em que corresponde attr_2(esta coluna representa o sexo, portanto, se essa solução foi personalizada, ela deve ser smallint(1)com índice simples, etc ...)
  • E, ao mesmo tempo correspondência pelo menos um dos 1980, 1981, 1982, 1983 or 1984emattr_3
  • E ao mesmo tempo correspondente 2ou 3emattr_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.

Martin
fonte
1
Acho muito improvável que esse design tenha um desempenho melhor que o design original com índices compostos. Quais testes você fez para compará-los?
precisa saber é o seguinte
0

Tente usar dicas de índice de consulta que pareçam apropriadas

Dicas do índice Mysql

Muhammad Muazzam
fonte
1
Dicas podem ajudar uma versão da consulta, mas depois prejudicar outra. Observe que o Optimizer escolheu o ano como a melhor primeira tabela, provavelmente porque se filtrou as linhas mais indesejáveis.
Rick James