Actualização das páginas órfãs

October 18th, 2009 ntavares Posted in pt_PT, wikipedia No Comments »

Ouvir com webReader

A pedido do Lijealso, aqui vai uma actualização das estatísticas incompletas da Wikipédia lusófona para o caso das páginas órfãs.

Constatou-se que o dump utilizado anteriormente era insuficiente, pelo que se descarregou a tabela pagelinks, desta vez do dump de 20091015. Para se excluir os redireccionamentos, importou-se também a tabela redirect.

Fartei-me entretanto de alternar entre o que estava a fazer e a lista de códigos de domínios, pelo que criei uma pequena tabela auxiliar:

MySQL:
  1. mysql> CREATE TABLE _namespaces ( id TINYINT NOT NULL, namespace VARCHAR(50), PRIMARY KEY (id) );
  2. Query OK, 0 rows affected (0.01 sec)
  3.  
  4. mysql> INSERT INTO _namespaces VALUES (-2,'Media'),(-1,'Especial'),(0,''),(1,'Discussão'),(2,'Usuário'),(3,'Usuário Discussão'),(4,'Wikipedia'),(5,'Wikipedia Discussão'),(6,'Ficheiro'),(7,'Ficheiro Discussão'),(8,'MediaWiki'),(9,'MediaWiki Discussão'),(10,'Predefinição'),(11,'Predefinição Discussão'),(12,'Ajuda'),(13,'Ajuda Discussão'),(14,'Categoria'),(15,'Categoria Discussão'),(100,'Portal'),(101,'Portal Discussão'),(102,'Anexo'),(103,'Anexo Discussão');
  5. Query OK, 22 rows affected (0.00 sec)
  6. Records: 22  Duplicates: 0  WARNINGS: 0

O resultado deu-me um incrível total de 769854 páginas órfãs, pelo que decidi separá-las por namespace para permitir prioritizar a análise:

MySQL:
  1. mysql> SELECT p.page_namespace,count(1) FROM page p
  2.     -> LEFT JOIN redirect  r
  3.     -> ON p.page_id = r.rd_from
  4.     ->
  5.     -> LEFT JOIN pagelinks pl
  6.     -> on pl.pl_namespace = p.page_namespace
  7.     -> and pl.pl_title = p.page_title
  8.     ->
  9.     -> WHERE r.rd_from IS NULL
  10.     -> AND pl.pl_from IS NULL
  11.     ->
  12.     -> GROUP BY p.page_namespace;
  13. +----------------+----------+
  14. | page_namespace | count(1) |
  15. +----------------+----------+
  16. |              0 |    12958 |
  17. |              1 |   103645 |
  18. |              2 |    16592 |
  19. |              3 |   568675 |
  20. |              4 |     1954 |
  21. |              5 |      856 |
  22. |              8 |      773 |
  23. |              9 |       17 |
  24. |             10 |     7522 |
  25. |             11 |     1014 |
  26. |             12 |        3 |
  27. |             13 |       27 |
  28. |             14 |    51735 |
  29. |             15 |     1315 |
  30. |            100 |     1190 |
  31. |            101 |      117 |
  32. |            102 |      173 |
  33. |            103 |     1288 |
  34. +----------------+----------+
  35. 18 rows in SET (20.90 sec)

O resultado do cruzamento das duas tabelas foi afixado aqui, com uma listagem de 15M para os 12958 artigos no domínio principal. Na verdade, esta listagem foi feita para colar numa página wiki, no entanto tenham em atenção que são 15M, pelo que não recomendo fazê-lo. Têm outras listas (como a mais simples, em formato pageid,namespace,title) nessa directoria.

AddThis Social Bookmark Button

Revisita aos dumps da Wikipédia

October 6th, 2009 ntavares Posted in pt_PT, wikipedia 1 Comment »

Ouvir com webReader

Desta vez em português, decidi dar [alguma] continuidade ao que comecei há uns dias com a importação dos dumps da Wikipédia. Graças à dica do Rei-artur foi fácil extrair a lista de robôs, para excluir das estatísticas.

CODE:
  1. [myself@speedy ~]# wget 'http://pt.wikipedia.org/w/api.php?action=query&list=allusers&aufrom=A&augroup=bot&aulimit=500&format=txt' -q -O - > bots.tmp
  2.  
  3. [myself@speedy ~]# cat bots.tmp | grep '\[name\]' | sed 's,^.*\[name\] => ,,' > /tmp/bots.txt

Aproveitei e repesquei os user_id para simplificar as pesquisas sem fazer alterações na tabela user.

MySQL:
  1. mysql> CREATE TABLE user_bots ( bot_name VARCHAR(25) );
  2. Query OK, 0 rows affected (0.01 sec)
  3.  
  4. mysql> LOAD DATA INFILE '/tmp/bots.txt' INTO table user_bots;
  5. Query OK, 136 rows affected (0.00 sec)
  6. Records: 136  Deleted: 0  Skipped: 0  WARNINGS: 0
  7.  
  8.  
  9. mysql> ALTER TABLE user_bots add COLUMN bot_user_id INT;
  10. Query OK, 136 rows affected (0.01 sec)
  11. Records: 136  Duplicates: 0  WARNINGS: 0
  12.  
  13. mysql> ALTER TABLE user add index idx_t ( user_name );
  14. Query OK, 119134 rows affected (2.63 sec)
  15. Records: 119134  Duplicates: 0  WARNINGS: 0
  16.  
  17. mysql> UPDATE user_bots ub JOIN user u on user_name = bot_name SET ub.bot_user_id = u.user_id;
  18. Query OK, 134 rows affected (0.00 sec)
  19. Rows matched: 134  Changed: 134  WARNINGS: 0
  20.  
  21. mysql> ALTER TABLE user_bots add PRIMARY KEY (bot_user_id);
  22. Query OK, 136 rows affected, 1 warning (0.00 sec)
  23. Records: 136  Duplicates: 0  WARNINGS: 1
  24.  
  25. mysql> SHOW WARNINGS;
  26. +---------+------+---------------------------------------------------+
  27. | Level   | Code | Message                                           |
  28. +---------+------+---------------------------------------------------+
  29. | Warning | 1265 | Data truncated for COLUMN 'bot_user_id' at row 71 |
  30. +---------+------+---------------------------------------------------+
  31. 1 row in SET (0.00 sec)
  32.  
  33. mysql> UPDATE user_bots SET bot_user_id = -1 WHERE bot_user_id = 0;
  34. Query OK, 1 row affected (0.00 sec)
  35. Rows matched: 1  Changed: 1  WARNINGS: 0

Não tinha reparado que havia um utilizador/robô com o nome "MediaWiki default" mas, bem, depois de criar a Primary Key ficou com o bot_user_id=0 e, para evitar que coincidisse com o agregado para anonymous, dei-lhe o bot_user_id=-1.

Então agora já estamos prontos a completar a query onde ficámos no último dia (número de edições em artigos distintos em cada namespace por utilizador):

MySQL:
  1. mysql> EXPLAIN SELECT epn.user_name,epn.page_namespace,epn.edits
  2.     -> FROM edits_per_namespace epn
  3.     -> LEFT JOIN user_bots ub ON epn.user_id = ub.bot_user_id
  4.     -> WHERE ub.bot_user_id IS NULL
  5.     -> AND epn.user_id <> 0
  6.     -> ORDER BY edits desc limit 20;
  7. +----+-------------+-------+--------+---------------+---------+---------+----------------------+--------+--------------------------------------+
  8. | id | select_type | table | type   | possible_keys | key     | key_len | ref                  | rows   | Extra                                |
  9. +----+-------------+-------+--------+---------------+---------+---------+----------------------+--------+--------------------------------------+
  10. 1 | SIMPLE      | epn   | ALL    | NULL          | NULL    | NULL    | NULL                 | 187624 | USING WHERE; USING filesort          |
  11. 1 | SIMPLE      | ub    | eq_ref | PRIMARY       | PRIMARY | 4       | ntavares.epn.user_id |      1 | USING WHERE; USING index; NOT EXISTS |
  12. +----+-------------+-------+--------+---------------+---------+---------+----------------------+--------+--------------------------------------+
  13. 2 rows in SET (0.00 sec)
  14.  
  15. mysql> SELECT epn.user_name,epn.page_namespace,epn.edits
  16.     -> FROM edits_per_namespace epn
  17.     -> LEFT JOIN user_bots ub ON epn.user_id = ub.bot_user_id
  18.     -> WHERE ub.bot_user_id IS NULL
  19.     -> AND epn.user_id <> 0
  20.     -> ORDER BY edits desc limit 10;
  21. +----------------+----------------+-------+
  22. | user_name      | page_namespace | edits |
  23. +----------------+----------------+-------+
  24. | EMP,Nice poa   |              0 | 58138 |
  25. | Dantadd        |              0 | 44767 |
  26. | João Carvalho  |              3 | 44533 |
  27. | OS2Warp        |              0 | 43396 |
  28. | Yanguas,Sonlui |              0 | 37020 |
  29. | Lijealso       |              0 | 34157 |
  30. | Rei-artur      |              0 | 33863 |
  31. | Tumnus         |              3 | 33213 |
  32. | Nuno Tavares   |              0 | 31910 |
  33. | Bisbis         |              0 | 29886 |
  34. +----------------+----------------+-------+
  35. 10 rows in SET (0.76 sec)

Os resultados completos estão aqui.

Já agora, para finalizar, a tão afamada lista de wikipedistas por número de edições:

MySQL:
  1. mysql> CREATE TABLE edits_per_user SELECT rev_user,count(1) as counter FROM revision GROUP BY rev_user;
  2. Query OK, 119134 rows affected (12.61 sec)
  3. Records: 119134  Duplicates: 0  WARNINGS: 0
  4.  
  5. mysql> SELECT u.user_name,epu.counter
  6.     -> FROM edits_per_user epu
  7.     -> LEFT JOIN user_bots ub on ub.bot_user_id = epu.rev_user
  8.     -> JOIN user u on epu.rev_user = u.user_id
  9.     -> WHERE ub.bot_user_id IS NULL ORDER BY counter desc limit 10;
  10. +----------------+---------+
  11. | user_name      | counter |
  12. +----------------+---------+
  13. | anonymous      | 3119758 |
  14. | EMP,Nice poa   |  176338 |
  15. | OS2Warp        |  163751 |
  16. | Dantadd        |  105657 |
  17. | Lijealso       |   90025 |
  18. | Yanguas,Sonlui |   89152 |
  19. | Rei-artur      |   83662 |
  20. | Mschlindwein   |   75680 |
  21. | Bisbis         |   75361 |
  22. | Nuno Tavares   |   73141 |
  23. +----------------+---------+
  24. 10 rows in SET (0.05 sec)

Os resultados completos estão aqui.

AddThis Social Bookmark Button

Revisita aos dados estruturados

October 3rd, 2009 ntavares Posted in pt_PT, wikipedia No Comments »

Ouvir com webReader

Há alguns dias num mergulho profundo sobre a utilização de wikis em campos específicos deparei-me com uma "foto" da Wikipédia muito interessante, aqui, que ilustra, entre outras coisas, a actividade na Wikipédia, a vários níveis: Visualizing Science & Tech Activity in Wikipedia:


Fonte: A Beatiful WWW

O website, A Beatiful WWW, dedica-se à extracção e representação dos volumes de informação distintos que conhecemos hoje. Eu já tinha falado nisto e descobri, entretanto, que o Google disponibiliza uma API de representação de dados estruturados.

Consigo pensar numa série de brincadeiras para isto :) Imaginem, por exemplo, juntar isto tudo, logo agora que a Wikimedia vai estar empenhada em manter os conteúdos disponíveis no Amazon Public Data Sets!..

Olhem aqui um exemplo do que pode ser feito, desta vez com Hadoop e Hive: Hot Wikipedia Topics, Served Fresh Daily.

AddThis Social Bookmark Button

Tamanho e character set de campos VARCHAR e consequências

September 24th, 2009 ntavares Posted in mysql, performance, pt_PT 3 Comments »

Ouvir com webReader

Apesar do tamanho dos campos VARCHAR não influenciar o tamanho dos índices, este tipo de dados (e outros semelhantes) comportam uma série de características que podem afectar seriamente a performance. Vamos testar o impacto destes campos em dois cenários: com character sets diferentes e com tamanhos diferentes.

Tendo em conta que o storage engine MEMORY apenas trabalha com tamanhos de tuplos fixos (fixed-length rows), e que este engine é o utilizado para tabelas temporárias (uma coisa a evitar, embora nem sempre seja possível), as consequências podem ser desastrosas.

Para esta demonstração, vamos definir o tamanho máximo das tabelas MEMORY para um valor que possamos atingir em alguns segundos, o mínimo:

MySQL:
  1. mysql> SET max_heap_table_size = 1;
  2. Query OK, 0 rows affected, 1 warning (0.00 sec)
  3.  
  4. mysql> SHOW variables like '%heap%';
  5. +---------------------+-------+
  6. | Variable_name       | Value |
  7. +---------------------+-------+
  8. | max_heap_table_size | 16384 |
  9. +---------------------+-------+
  10. 1 row in SET (0.00 sec)

Observamos que o mínimo que conseguimos será 16KB. Vamos ver o que acontece com campos VARCHAR (ie, de comprimento [VAR]iável):

MySQL:
  1. mysql> CREATE TABLE `varchar_small` (
  2.     ->   `id` VARCHAR(36) NOT NULL
  3.     -> ) ENGINE=MEMORY DEFAULT CHARSET=utf8;
  4. Query OK, 0 rows affected (0.01 sec)
  5.  
  6. mysql> INSERT INTO varchar_small VALUES('abcd');
  7. Query OK, 1 row affected (0.00 sec)
  8.  
  9. mysql> -- Vamos enchendo ate nao dar mais....
  10. mysql> INSERT INTO varchar_small SELECT * FROM varchar_small;
  11. ERROR 1114 (HY000): The table 'varchar_small' is full
  12.  
  13. mysql> CREATE TABLE `var_char` (
  14.     ->   `id` VARCHAR(36) NOT NULL
  15.     -> ) ENGINE=MEMORY DEFAULT CHARSET=utf8;
  16. Query OK, 0 rows affected (0.00 sec)
  17.  
  18. mysql> INSERT INTO var_char VALUES('abcdefgh-ijkl-mnop-qrst-uvwxyzabcedf');
  19. Query OK, 1 row affected (0.00 sec)
  20.  
  21. mysql> -- O mesmo: vamos enchendo ate nao dar mais...
  22. mysql> INSERT INTO var_char SELECT * FROM var_char;
  23. ERROR 1114 (HY000): The table 'var_char' is full
  24.  
  25. mysql> SELECT count(1) FROM var_char;
  26. +----------+
  27. | count(1) |
  28. +----------+
  29. |      320 |
  30. +----------+
  31. 1 row in SET (0.00 sec)
  32.  
  33. mysql> SELECT count(1) FROM varchar_small;
  34. +----------+
  35. | count(1) |
  36. +----------+
  37. |      320 |
  38. +----------+
  39. 1 row in SET (0.00 sec)

Ora bem, o que fiz foi preencher a primeira tabela com conteúdo bem menor que 36 carácteres (apenas 4) e, na segunda, conteúdo que preenchesse o campo todo. O que podemos observar é que, neste storage engine, o espaço ocupado por um campo pouco cheio ou muito cheio é sempre o mesmo: é o tamanho total associado ao campo. É isso que significa fixed-length, e isto acarreta como consequência que o tamanho de um campo VARCHAR em tabelas do tipo MEMORY (leia-se também: tabelas temporárias) é invariavelmente o tamanho máximo do campo. Consequentemente, ainda que um tamanho máximo de 255 não influencie o tamanho de índice sobre esse campo, sempre que seja necessário transportar esses dados para tabelas temporárias pode verificar-se um desperdício enorme de espaço (multiplicar esse desperdício pelo número de linhas que terão que ser transportadas para a tabela temporária!).

Mas isto não fica por aqui: para além de cuidado e bom senso na definição do tamanho do campo, é necessário também ter em atenção o seu encoding. O segundo ponto é sobre a influência do charset. Tipicamente trabalhávamos em Latin1, mas com a disseminação da localização (i18n) passou-se praticamente a usar sempre UTF-8. Tudo muito bem, deixámos de ter uma série de problemas com a formatação das sequências de carácteres (vulgo strings). Mas nem tudo são rosas: o charset UTf-8 pode consumir até 4 carácteres (em MySQL, 3), ao passo que Latin1 usava apenas 1:

MySQL:
  1. mysql> CREATE TABLE `varchar_utf8` (
  2.     ->   `id` VARCHAR(36) NOT NULL
  3.     -> ) ENGINE=MEMORY DEFAULT CHARSET=utf8;
  4. Query OK, 0 rows affected (0.01 sec)
  5.  
  6. mysql> CREATE TABLE `varchar_latin1` (
  7.     ->   `id` VARCHAR(36) NOT NULL
  8.     -> ) ENGINE=MEMORY DEFAULT CHARSET=latin1;
  9. Query OK, 0 rows affected (0.00 sec)

A cláusula geral de encoding da tabela propaga-se como default para as colunas. De qualquer forma, poderia especificar o encoding para a coluna de forma individual. Para ter a certeza que não estamos a ser enganados pelo encoding corrente no cliente mysql, usei dois scripts em PHP para criar uma única linha para cada uma destas tabelas (o encoding no meu terminal é UTF-8, como já vai sendo comum):

PHP:
  1. mysql_connect('localhost','root','');
  2. mysql_query("SET NAMES UTF8");
  3. mysql_query("INSERT INTO varchar_utf8 VALUES('ãããããããã-ãããã-ãããã-ãããã-ãããããããããããã')");

PHP:
  1. mysql_connect('localhost','root','');
  2. mysql_query("SET NAMES Latin1");
  3. mysql_query("INSERT INTO varchar_latin1 VALUES('ãããããããã-ãããã-ãããã-ãããã-ãããããããããããã')");
  4. EOF

Gravei os dois scripts acima para varchar_utf8.php e varchar_latin1.php.temp, respectivamente. Por fim, alterei o encoding do script para em Latin1 (porque faça o que fizer, o encoding do meu terminal é UTF-8):

CODE:
  1. [root@speedy ~]# cat varchar_latin1.php.temp | iconv -f utf8 -t iso-8859-1 > varchar_latin1.php

E executei os dois:

CODE:
  1. [root@speedy ~]# php -f varchar_utf8.php
  2. [root@speedy ~]# php -f varchar_latin1.php

OK, agora tenho 1 registo de cada em cada tabela. Vamos usar esse único registo em cada para encher as tabelas até não ser possível adicionar mais registos:

MySQL:
  1. mysql> -- ...
  2. mysql> INSERT INTO varchar_utf8 SELECT * FROM varchar_utf8;
  3. ERROR 1114 (HY000): The table 'mem_utf8' is full
  4.  
  5. mysql> -- ...
  6. INSERT INTO varchar_latin1 SELECT * FROM varchar_latin1;
  7. ERROR 1114 (HY000): The table 'mem_latin1' is full
  8.  
  9. mysql> SELECT count(1) FROM mem_utf8;
  10. +----------+
  11. | count(1) |
  12. +----------+
  13. |      126 |
  14. +----------+
  15. 1 row in SET (0.00 sec)
  16.  
  17. mysql> SELECT count(1) FROM mem_latin1;
  18. +----------+
  19. | count(1) |
  20. +----------+
  21. |      320 |
  22. +----------+
  23. 1 row in SET (0.00 sec)

Pois é, verificámos o segundo problema: no engine MEMORY couberam muito menos linhas na primeira tabela (UTF-8) que na primeira (Latin1) porque que os campos são do seu tamanho máximo possível e, no caso dos VARCHAR em UTF8, cada carácter pode ocupar até 3 bytes. Ora, para caberem 36 carácteres, serão necessários, pelo menos, 36*3 = 108 bytes! Isto representa um consumo de 300% que pode não ter sido estimado no que respeita ao tamanho de memória permitido para tabelas temporárias.

Ambos cenários são bastante frequentes, especialmente em aplicações que usem frameworks - e também nestes casos, pela generalidade de backends que tentam suportar (MySQL, PostgreSQL, Oracle, etc), as queries envolvidas não costumam ser optimizadas para nenhum motor em particular; no caso do MySQL essas queries geram, muitas vezes, tabelas temporárias sem necessidade e optimizar este tipo de queries pode implicar alterações no núcleo da framework ou, no mínimo, criar excepções à regra.

A escolha do nome da coluna nos exemplos acima não foi ao acaso: com efeito, os campos UUID() costumam ser UTF-8 apenas porque toda a tabela é UTF-8 e, imagine-se, não há nenhum carácter num UUID() que não seja ASCII (na verdade, não há nenhum carácter que não seja [a-zA-Z-1-9]!):

MySQL:
  1. mysql> SELECT UUID();
  2. +--------------------------------------+
  3. | UUID()                               |
  4. +--------------------------------------+
  5. | 27d6a670-a64b-11de-866d-0017083bf00f |
  6. +--------------------------------------+
  7. 1 row in SET (0.00 sec)

Como o conjunto de caracteres de um UUID() é charset compatible entre os vários, esta é uma das optimizações que poderão fazer nas vossas aplicações facilmente, contribuindo para o aumento de performance do MySQL sem qualquer impacto na vossa aplicação.

AddThis Social Bookmark Button

Novidades na integração SugarCRM - IPBrick

September 15th, 2009 ntavares Posted in pt_PT, sugarcrm No Comments »

Ouvir com webReader

Para a versão GA do SugarCRM 5.5, estamos a preparar algumas surpresas para a versão 5.1 da IPBrick:

  • Disponibilização do suporte integrado para qualquer das versões SugarCRM 5.2 e 5.5 (Community Edition, Professional ou Enterprise).
  • Novo método de sincronização/importação de contas e contactos. Este método reduz exponencialmente a velocidade de sincronização: quanto maiores forem os dados a importar, maior será a diferença na rapidez.
  • Graças a este novo método vai ser possível também algum nível de sincronização bidireccional. Na verdade trata-se de uma fusão - tanto quanto possível - de dois registos em alterados em ambos lados. O utilizador poderá controlar como pretende os resultados:
    • sincronização estrita com o IP Contactos
    • sincronização dos comuns (entre IPBrick e SugarCRM) mas preservando os dados isolados do SugarCRM (ie, que não existam no IPBrick), permitindo ao SugarCRM desenvolver autonomia
    • ou sincronização de apenas os dados novos do IP Contactos, preservando por completo os registos do SugarCRM.
  • Melhor integração com o SugarCRM: a nova versão está muito mais robusta no que toca a alterações upgrade safe e a DRI refez a integração para isso mesmo, o que significa que novas versões serão lançadas mais rapidamente.
  • A existência de uma camada de abstracção, possibilitando testar directa e instantaneamente sobre dados reais dos clientes. Ainda será desenvolvida uma funcionalidade de ofuscamento de dados para permitir manter a confidencialidade desses dados.
  • A possibilidade de realizar sobre o capítulo de sincronização uma bateria de testes de validação (unit testing) automatizados. Esta medida vai-nos possibilitar fazer controlo de qualidade antes de cada versão do módulo.
  • E claro, não menos importante, um footprint de memória muito reduzido (< 1MB na linha de comandos);
  • A nova interface de administração, com um assistente que vai explicando os passos a seguir, oferece agora a possibilidade de extrair relatórios da sincronização (ver abaixo) e permite a revisão do resultado final antes de ser fundido com o SugarCRM:

Quadro inicial:
sugar-ipbrick-uirevamp1

O primeiro passo é a importação dos dados do IP Contacts e cruzamento com os dados actuais do SugarCRM. No final do processo será possível rever as operações:
sugar-ipbrick-uirevamp2

Finalmente, o último passo é a fusão:
sugar-ipbrick-uirevamp3

De resto, estamos ainda a afinar os últimos detalhes do módulo de integração com as comunicações unificadas, que será também adaptado para as alterações da IPBrick 5.2.

AddThis Social Bookmark Button

SugarCRM data generator

August 31st, 2009 ntavares Posted in performance, pt_PT, sugarcrm No Comments »

Ouvir com webReader

O SugarCRM dispõe de um gerador de dados para popular a DB com accounts, contacts, etc de exemplo. No entanto, pode ser utilizado para gerar DB's para testes, nomeadamente para testes de carga. Segundo o que percebi, este data generator usa um seed fixo para que os dados gerados para uma DB sejam os mesmos para outra, para que se possam comparar entre si, por exemplo. Eis os passos necessários [usados na versão 5.5]:

Procurar large_scale_test no config.php (próx. à linha 200) e alterar para true:

CODE:
  1. 'large_scale_test' => false,
  2. 'large_scale_test' => true,

Colocar o script seguinte em install/dataGeneratorKit.php. Este script servirá de wrapper para o populateSeedData.php que já dispõe da lógica para popular a DB em termos propocionais:

PHP:
  1. define('sugarEntry',1);
  2. require_once('include/entryPoint.php');
  3. require_once('install/install_utils.php');
  4.  
  5. require_once('modules/TableDictionary.php');
  6.  
  7. require_once "include/database/DBManagerFactory.php";
  8. include "install/populateSeedData.php";
  9. ?>

Talvez seja necessário comentar algumas linhas dos ficheiros i18n, parecidas às que se mostram abaixo (fazem referência a SugarThemeRegistry). Pelo menos em 5.5beta foi:

PHP:
  1. ...
  2. //    'LBL_EMAIL_ADDRESS_BOOK_TITLE_ICON'     => 'getImageURL('icon_email_addressbook.gif').' align=absmiddle border=0> Address Book',
  3. //    'LBL_EMAIL_ADDRESS_BOOK_TITLE_ICON_SHORT'     => 'getImageURL('icon_email_addressbook.gif').' align=absmiddle border=0> Addr...',
  4. ...

Se necessário, alterar os valores no ficheiro install/populateSeedData.php:

PHP:
  1. if($large_scale_test) {
  2.         // increase the cuttoff time to 1 hour
  3.         ini_set("max_execution_time", "3600");
  4.         $number_contacts = 100000;
  5.         $number_companies = 15000;
  6.         $number_leads = 100000;
  7. }

Finalmente, executar:

CODE:
  1. php -f install/dataGeneratorKit.php

Notas

  • Não sei até que ponto o script é eficiente, pois já me crashou com ''memory exhausted'' nos 256MB de memory_limit.

Aproveitei e copiei este artigo para a Wiki da SugarCRM.

AddThis Social Bookmark Button

Percorrer uma tabela MyISAM de forma controlada

August 26th, 2009 ntavares Posted in mysql, pt_PT No Comments »

Ouvir com webReader

Lembrei-me de partilhar um teste que fiz há tempos. O que se pretendia era demonstrar que podemos percorrer uma tabela MyISAM (e só neste caso específico) de forma controlada (ie, prever a ordem pela qual os registos são devolvidos) e se esse percurso se mantém em outras operações para além do SELECT, como no caso do DELETE. É fácil pensar numa aplicação: se eu quiser, por exemplo, transladar blocos de registos de uma tabela para outra, torna-se fundamental que a operação DELETE também obedeça ao expectável já que, como sabemos, o MyISAM não é transaccional e, se alguma coisa falhar, queremos poder ter acesso aos registos desta forma determinística para saber o que reverter.

Para alguns pode parecer óbvio, mas sem olhar para o código nunca vamos ter a certeza. Para além disso, o código pode mudar, por isso mais vale termos a certeza :-) Não vamos sequer tentar extrapolar as conclusões para InnoDB porque internamente trabalha de forma completamente diferente. Aliás, um único aspecto da sua arquitectura - o famoso clustered index, que pode ou não ser interno, mas existe sempre! - dá logo para desconfiar que o comportamento seja completamente diferente.

Portanto, na prática, o que se pretende mesmo é ter a certeza que sabemos que registos vão surgindo em várias iterações, e se essa certeza se extrapola para DELETEs (e, eventualmente, para UPDATEs) - ie, tornar o nosso processo determinístico.

Vamos começar com uma tabela simples:

MySQL:
  1. CREATE TABLE `teste` (
  2.   `_int` INT(11) NOT NULL DEFAULT '0',
  3.   `_char` VARCHAR(5) NOT NULL DEFAULT '',
  4.   KEY `idx_int` (`_int`)
  5. ) ENGINE=MyISAM DEFAULT CHARSET=latin1

E inserimos alguns dados:

MySQL:
  1. mysql> INSERT INTO teste VALUES (2,'c'), (1,'e'), (1,'b'), (1,'z'), (2,'b'), (2,'d'),(3,'m');
  2. Query OK, 6 rows affected (0.00 sec)
  3. Records: 6  Duplicates: 0  WARNINGS: 0
  4.  
  5. mysql> SELECT SQL_NO_CACHE * FROM teste;
  6. +------+-------+
  7. | _int | _char |
  8. +------+-------+
  9. |    2 | c     |
  10. |    1 | e     |
  11. |    1 | b     |
  12. |    1 | z     |
  13. |    2 | b     |
  14. |    2 | d     |
  15. |    3 | m     |
  16. +------+-------+
  17. 7 rows in SET (0.00 sec)

A ordem pela qual foram inseridos os registos é fundamental. Podemos observar que este table scan é feito de forma natural, também segundo o Query Optimizer:

MySQL:
  1. mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM teste;
  2. +----+-------------+-------+------+---------------+------+---------+------+------+-------+
  3. | id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra |
  4. +----+-------------+-------+------+---------------+------+---------+------+------+-------+
  5. 1 | SIMPLE      | teste | ALL  | NULL          | NULL | NULL    | NULL |    7 |       |
  6. +----+-------------+-------+------+---------------+------+---------+------+------+-------+
  7. 1 row in SET (0.00 sec)

Perfeito. Estou a pedir os campos todos de cada linha, onde se inclui _char, que não é indexado, e o Optimizer comporta-se como suposto. Mas agora vejamos uma query ligeiramente diferente:

MySQL:
  1. mysql> EXPLAIN SELECT SQL_NO_CACHE _int FROM teste;
  2. +----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
  3. | id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows | Extra       |
  4. +----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
  5. 1 | SIMPLE      | teste | index | NULL          | idx_int | 4       | NULL |    7 | USING index |
  6. +----+-------------+-------+-------+---------------+---------+---------+------+------+-------------+
  7. 1 row in SET (0.00 sec)

É interessante como o Optimizer reconheceu que, se eu só quero um campo que por acaso até está indexado, então posso obtê-lo directamente do índice (e é isso que significa Using index no campo Extra) evitando ir aos datafiles. Porém isto significa que iremos obter os registos pela ordem do índice:

MySQL:
  1. mysql> SELECT SQL_NO_CACHE _int FROM teste;
  2. +------+
  3. | _int |
  4. +------+
  5. |    1 |
  6. |    1 |
  7. |    1 |
  8. |    2 |
  9. |    2 |
  10. |    2 |
  11. |    3 |
  12. +------+
  13. 7 rows in SET (0.00 sec)

Isto é mais importante que o que possa parecer para este teste. Se eu for obrigado a requisitar mais campos do que esse, o Optimizer vai voltar ao table scan. E não basta colocar um ORDER BY...

MySQL:
  1. mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM teste ORDER BY _int;
  2. +----+-------------+-------+------+---------------+------+---------+------+------+----------------+
  3. | id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra          |
  4. +----+-------------+-------+------+---------------+------+---------+------+------+----------------+
  5. 1 | SIMPLE      | teste | ALL  | NULL          | NULL | NULL    | NULL |    7 | USING filesort |
  6. +----+-------------+-------+------+---------------+------+---------+------+------+----------------+
  7. 1 row in SET (0.00 sec)

... porque o Optimizer pode querer usar uma tabela temporária para fazer a ordenação, que é que nos diz Using filesort no campo Extra. Isto pode parecer uma falha do Optimizer, mas a verdade é que o Optimizer é inteligente o suficiente para determinar que um ''full table scan'' pode ser mais eficiente que percorrer o índice por uma ordem e ir buscar os restantes dados aos data files com localização (nos discos) completamente desordenada, provavelmente aleatória (o I/O manifestar-se-ia imediatamente) - claro que nesta tabela talvez não seja o caso, mas para tabelas muito grandes pode ser desastroso. Assim sendo, teríamos que forçar explicitamente a utilização do índice, já que, pelo menos no meu caso, nem a pista USE INDEX ajudava:

MySQL:
  1. mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM teste FORCE INDEX(idx_int) ORDER BY _int;
  2. +----+-------------+-------+-------+---------------+---------+---------+------+------+-------+
  3. | id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows | Extra |
  4. +----+-------------+-------+-------+---------------+---------+---------+------+------+-------+
  5. 1 | SIMPLE      | teste | index | NULL          | idx_int | 4       | NULL |    7 |       |
  6. +----+-------------+-------+-------+---------------+---------+---------+------+------+-------+
  7. 1 row in SET (0.00 sec)

De facto, o Optimizer é tão teimoso que mesmo forçando a utilização do índice ele descarta-o se não usarmos o ORDER BY, pois sabe que, para um table scan a ordem dos registos é indiferente e, como tal, não precisa do índice para nada. Deve haver uma explicação para este comportamento - que vou ter que pesquisar - mas este comportamento interessa-nos e muito: se o Optimizer pegasse ao acaso um índice que lhe parecesse bem, seria difícil obter os registos pela ordem natural sem testar com um EXPLAIN primeiro. Parece-me interessante salientar o seguinte:

MySQL:
  1. mysql> SELECT SQL_NO_CACHE * FROM teste FORCE INDEX(idx_int) ORDER BY _int;
  2. +------+-------+
  3. | _int | _char |
  4. +------+-------+
  5. |    1 | e     |
  6. |    1 | b     |
  7. |    1 | z     |
  8. |    2 | c     |
  9. |    2 | b     |
  10. |    2 | d     |
  11. |    3 | m     |
  12. +------+-------+
  13. 7 rows in SET (0.00 sec)

Ou seja, dentro do índice, quando há colisões, elas são simplesmente adicionadas no fim. Isto significa que, após a ordenação, a ordem pela qual obtemos os registos é... a ordem natural.

Mas pronto, agora sim, podemos assumir, para já, que se percorrermos a tabela com SELECT ... LIMIT 1, podemos ir obtendo registo a registo quer pela ordem natural, quer pela ordem do índice que quisermos. Mas o meu grande problema era na remoção, pois não temos EXPLAIN para o DELETE. Qual dos dois métodos o DELETE utiliza?

MySQL:
  1. mysql> DELETE FROM teste LIMIT 1;
  2. Query OK, 1 row affected (0.00 sec)
  3.  
  4. mysql> SELECT SQL_NO_CACHE * FROM teste;
  5. +------+-------+
  6. | _int | _char |
  7. +------+-------+
  8. |    1 | e     |
  9. |    1 | b     |
  10. |    1 | z     |
  11. |    2 | b     |
  12. |    2 | d     |
  13. |    3 | m     |
  14. +------+-------+
  15. 6 rows in SET (0.00 sec)

Bem, para já, parece ser a ordem natural. Claro que se eu especificar um ORDER BY _int o próximo registo a apagar deveria ser (1,e) - porque é o primeiro no índice idx_int, e sabemos nós que o valor no campo _char será o da ordem natural - resta saber se o Optimizer não pensa que precisa duma tabela temporária para essa ordenação. Eu estou convencido que não, pois como não há selecção de nenhum campo específico, não há porque não utilizar o índice idx_int;. Vamos só confirmar:

MySQL:
  1. mysql> DELETE FROM teste ORDER BY _int LIMIT 1;
  2. Query OK, 1 row affected (0.00 sec)
  3.  
  4. mysql> SELECT SQL_NO_CACHE * FROM teste;
  5. +------+-------+
  6. | _int | _char |
  7. +------+-------+
  8. |    1 | b     |
  9. |    1 | z     |
  10. |    2 | b     |
  11. |    2 | d     |
  12. |    3 | m     |
  13. +------+-------+
  14. 5 rows in SET (0.00 sec)

Tudo bem, conforme previsto. Mas há mais. Raramente há tabelas só com um índice e pode acontecer que o nosso campo _char fosse apanhado por um índice, o que tornaria as coisas um pouco diferentes:

MySQL:
  1. mysql> ALTER TABLE teste ADD KEY idx_char(_char);
  2. Query OK, 5 rows affected (0.00 sec)
  3. Records: 5  Duplicates: 0  WARNINGS: 0
  4.  
  5. mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM teste;
  6. +----+-------------+-------+------+---------------+------+---------+------+------+-------+
  7. | id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra |
  8. +----+-------------+-------+------+---------------+------+---------+------+------+-------+
  9. 1 | SIMPLE      | teste | ALL  | NULL          | NULL | NULL    | NULL |    5 |       |
  10. +----+-------------+-------+------+---------------+------+---------+------+------+-------+
  11. 1 row in SET (0.00 sec)

Acho interessante porque é como se o Optimizer «em caso de dúvida, optasse por nenhum», ou seja, como desconhece um critério para escolher um ou outro índice, não usa nenhum.

Na verdade, o Optimizer não usa nenhum índice porque não tem nenhuma pista sobre qual agarrar. Por norma irá utilizar o de maior cardinalidade, para as pistas que tiver disponíveis:

MySQL:
  1. mysql> OPTIMIZE TABLE teste;
  2. +------------+----------+----------+----------+
  3. | Table   | Op      | Msg_type | Msg_text
  4. +------------+----------+----------+----------+
  5. | test.teste | OPTIMIZE | status   | OK    
  6. +------------+----------+----------+----------+
  7. 1 row in SET (0.00 sec)
  8.  
  9. mysql> SHOW INDEXES FROM teste;
  10. +-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
  11. | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | NULL | Index_type | Comment
  12. +-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
  13. | teste |        1 | idx_int  |         1 | _int      | A         |       2 |     NULL | NULL   |      | BTREE     |        
  14. | teste |        1 | idx_char |         1 | _char     | A         |       5 |     NULL | NULL   |      | BTREE     |        
  15. +-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
  16. 2 rows in SET (0.00 sec)
  17.  
  18. mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM teste WHERE _int = 1 AND _char = 'e';
  19. +----+-------------+-------+------+------------------+----------+---------+-------+------+-------------+
  20. | id | select_type | table | type | possible_keys   | key    | key_len | ref   | rows | Extra      
  21. +----+-------------+-------+------+------------------+----------+---------+-------+------+-------------+
  22. 1 | SIMPLE     | teste | ref  | idx_int,idx_char | idx_char | 7    | const | 1 | USING WHERE
  23. +----+-------------+-------+------+------------------+----------+---------+-------+------+-------------+
  24. 1 row in SET (0.00 sec)

Ou seja, idx_char é utilizado para a filtragem e, como tinha potencial para filtrar mais registos, é esse índice o escolhido pelo Optimizer, que nos diz ainda que vai precisar de percorrer os datafiles para filtrar o campo idx_int (Using where).

Eu sei que o * é expandido para os campos todos pelo Optimizer; então e se houvesse um covering index?

MySQL:
  1. mysql> ALTER TABLE teste ADD KEY idx_int_char(_int,_char);
  2. Query OK, 5 rows affected (0.01 sec)
  3. Records: 5  Duplicates: 0  WARNINGS: 0
  4.  
  5. mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM teste;
  6. +----+-------------+-------+-------+---------------+--------------+---------+------+------+-------------+
  7. | id | select_type | table | type  | possible_keys | key          | key_len | ref  | rows | Extra      
  8. +----+-------------+-------+-------+---------------+--------------+---------+------+------+-------------+
  9. 1 | SIMPLE     | teste | index | NULL        | idx_int_char | 11    | NULL |    5 | USING index
  10. +----+-------------+-------+-------+---------------+--------------+---------+------+------+-------------+
  11. 1 row in SET (0.00 sec)

Como era de esperar, o índice será usado. Vamos ver o que acontece com o DELETE. Para testar, forçamos os registos a mudar a posição no índice (mantendo a ordem natural) e vamos criar outra tabela similar, com os mesmos registos, porque vai-nos interessar esta ordem mais abaixo:

MySQL:
  1. mysql> UPDATE teste SET _int = 4 WHERE _int = 1 AND _char = 'b'; UPDATE teste SET _int = 5, _char = 'f' WHERE _int = 2 AND _char = 'b';
  2.  
  3. mysql> CREATE TABLE teste3 like teste;
  4. Query OK, 0 rows affected (0.00 sec)
  5.  
  6. mysql> INSERT INTO teste3 SELECT * FROM teste IGNORE INDEX(idx_int_char);
  7. Query OK, 5 rows affected (0.00 sec)
  8. Records: 5  Duplicates: 0  WARNINGS: 0
  9.  
  10. mysql> SELECT * FROM teste3 IGNORE INDEX(idx_int_char);
  11. +------+-------+
  12. | _int | _char |
  13. +------+-------+
  14. |    4 | b     |
  15. |    1 | z     |
  16. |    5 | f     |
  17. |    2 | d     |
  18. |    3 | m     |
  19. +------+-------+
  20. 5 rows in SET (0.00 sec)
  21.  
  22. mysql> DELETE FROM teste3 LIMIT 1;
  23. Query OK, 1 row affected (0.00 sec)
  24.  
  25. mysql> SELECT * FROM teste3 IGNORE INDEX(idx_int_char);
  26. +------+-------+
  27. | _int | _char |
  28. +------+-------+
  29. |    1 | z     |
  30. |    5 | f     |
  31. |    2 | d     |
  32. |    3 | m     |
  33. +------+-------+
  34. 4 rows in SET (0.00 sec)

Ou seja, com o SELECT antes e depois temos oportunidade de comprovar que o DELETE não pegou nenhum índice, nem mesmo o covering index.

Mas voltando atrás, à tabela teste, será que o facto de se usar uma PRIMARY KEY (que é única e não nula) influencia os resultados? Esta pergunta provavelmente só será pertinente para quem conheça o InnoDB.

MySQL:
  1. mysql> ALTER TABLE teste DROP KEY idx_int;
  2. Query OK, 5 rows affected (0.01 sec)
  3. Records: 5  Duplicates: 0  WARNINGS: 0
  4.  
  5. mysql> ALTER TABLE teste ADD PRIMARY KEY(_int);
  6. Query OK, 5 rows affected (0.01 sec)
  7. Records: 5  Duplicates: 0  WARNINGS: 0
  8.  
  9. mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM teste;
  10. +----+-------------+-------+------+---------------+------+---------+------+------+-------+
  11. | id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra |
  12. +----+-------------+-------+------+---------------+------+---------+------+------+-------+
  13. 1 | SIMPLE      | teste | ALL  | NULL          | NULL | NULL    | NULL |    5 |       |
  14. +----+-------------+-------+------+---------------+------+---------+------+------+-------+
  15. 1 row in SET (0.00 sec)

Muito bem, nada a assinalar.

Mas nós também sabemos o que acontece com o MyISAM: após os DELETEs começam a surgir buracos nos data files (que, consequentemente, acabam por interferir com as inserções em concorrência). Vejamos. Recriei a tabela teste com o dataset original, e fiz os dois DELETE que fizémos até aqui. A seguir:

MySQL:
  1. mysql> SELECT @@concurrent_insert;
  2. +---------------------+
  3. | @@concurrent_insert
  4. +---------------------+
  5. |               1
  6. +---------------------+
  7. 1 row in SET (0.00 sec)
  8.  
  9. mysql> INSERT INTO teste VALUES (6,'e'),(5,'f'),(4,'g');
  10. Query OK, 3 rows affected (0.00 sec)
  11. Records: 3  Duplicates: 0  WARNINGS: 0
  12.  
  13. mysql> SELECT SQL_NO_CACHE * FROM teste;
  14. +------+-------+
  15. | _int | _char |
  16. +------+-------+
  17. |   5 | f     |
  18. |   6 | e     |
  19. |   1 | b     |
  20. |   1 | z     |
  21. |   2 | b     |
  22. |   2 | d     |
  23. |   3 | m     |
  24. |   4 | g     |
  25. +------+-------+
  26. 8 rows in SET (0.00 sec)

Repare-se que os dois primeiros registos ficaram fora da ordem natural pois o MyISAM conseguiu reciclar o espaço livre, exactamente nos mesmos sítios. O terceiro elemento calhou no único sítio onde cabia: no fim. Esta também é uma observação importante porque se houverem escritas na tabela durante as operações, então é preciso um cuidado especial. Este cuidado agrava-se pelo facto de nem conseguirmos desactivar este comportamento. Recrie-se novamente a tabela teste, com o INSERT inicial, depois os dois DELETEs feitos até agora, e depois o INSERT do teste anterior:

MySQL:
  1. mysql> SET global concurrent_insert = 0;
  2. Query OK, 0 rows affected (0.00 sec)
  3.  
  4. mysql> INSERT INTO teste VALUES (6,'e'),(5,'f'),(4,'g');
  5. Query OK, 3 rows affected (0.00 sec)
  6. Records: 3  Duplicates: 0  WARNINGS: 0
  7.  
  8. mysql> SELECT SQL_NO_CACHE * FROM teste;
  9. +------+-------+
  10. | _int | _char |
  11. +------+-------+
  12. |   5 | f     |
  13. |   6 | e     |
  14. |   1 | b     |
  15. |   1 | z     |
  16. |   2 | b     |
  17. |   2 | d     |
  18. |   3 | m     |
  19. |   4 | g     |
  20. +------+-------+
  21. 8 rows in SET (0.00 sec)

Exactamente o mesmo. Perde-se, portanto, a ordem natural.

Nota sobre o InnoDB

A título de nota, deixa-se uma breve explicação do porquê de não analisarmos o caso do InnoDB:

MySQL:
  1. mysql> CREATE TABLE teste2 like teste;
  2. Query OK, 0 rows affected (0.00 sec)
  3.  
  4. mysql> INSERT INTO teste2 SELECT * FROM teste;
  5. Query OK, 5 rows affected (0.01 sec)
  6. Records: 5  Duplicates: 0  WARNINGS: 0
  7.  
  8. mysql> UPDATE teste2 SET _char = 'k' WHERE _int = 1 and _char = 'b';
  9. Query OK, 1 row affected (0.03 sec)
  10. Rows matched: 1  Changed: 1  WARNINGS: 0
  11.  
  12. mysql> ALTER TABLE teste2 engine=INNODB, DROP INDEX idx_int, DROP INDEX idx_char, DROP INDEX idx_int_char;
  13. Query OK, 5 rows affected (0.01 sec)
  14. Records: 5  Duplicates: 0  WARNINGS: 0
  15.  
  16. mysql> ALTER TABLE teste2 add PRIMARY KEY (_char);
  17. Query OK, 5 rows affected (0.02 sec)
  18. Records: 5  Duplicates: 0  WARNINGS: 0
  19.  
  20. mysql> EXPLAIN SELECT SQL_NO_CACHE * FROM teste2;
  21. +----+-------------+--------+------+---------------+------+---------+------+------+-------+
  22. | id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows | Extra
  23. +----+-------------+--------+------+---------------+------+---------+------+------+-------+
  24. 1 | SIMPLE     | teste2 | ALL  | NULL        | NULL | NULL    | NULL |    5 |    
  25. +----+-------------+--------+------+---------------+------+---------+------+------+-------+
  26. 1 row in SET (0.00 sec)
  27.  
  28. mysql> SELECT * FROM teste2;
  29. +------+-------+
  30. | _int | _char |
  31. +------+-------+
  32. |   2 | b     |
  33. |   2 | d     |
  34. |   1 | k     |
  35. |   3 | m     |
  36. |   1 | z     |
  37. +------+-------+
  38. 5 rows in SET (0.00 sec)

O que queria que reparassem é que não há indicação de estar a ser utilizada nenhuma chave e, no entanto, os registos vêm ordenados pela PRIMARY KEY. Como disse logo no início, com InnoDB os resultados seriam diferentes, e isto deve-se ao clustered index deste storage engine, que armazena os registos (fiscamente nos datafiles) pela ordem da chave (a famosa primary key order). Como consequência, não é possível ter acesso aos registos na forma natural (ie, pela ordem em que foram inseridos) - e, de forma consistente com o que tínhamos visto para MyISAM, o Optimizer prefere não usar nenhum índice, passando para o storage engine a mesma query expandida.

Penso que consegui demonstrar com alguma segurança que podemos obter registos com SELECT de uma forma controlada. Dependendo do cenário, nem sempre se torna possível usar um ou outro método (também pode não ser possível criar um novo índice, por exemplo) pelo que antes de tirar conclusões precipitadas, o melhor é usar o EXPLAIN para explorar as possibilidades!

AddThis Social Bookmark Button

A importância da Wikipédia enquanto fonte de dados e não [tanto] de informação

August 10th, 2009 ntavares Posted in pt_PT, wikipedia 3 Comments »

Ouvir com webReader

Tão cedo comecei a ganhar destreza na Wikipédia, não pude evitar lamentar-me com o desperdício, em termos de esforço, da criação de artigos em texto corrido a partir de dados na forma bruta - não havia, aparentemente, grande forma de contornar. Com efeito, os artigos da Wikipédia são pautados por relações intrínsecas de dados sobre determinado assunto, e digeridos numa determinada língua para que nos sejam facultados na forma de informação, o que faz com que se tornem mais ou menos eloquentes, menos brutos, mas menos isolados, menos reutilizáveis. Por exemplo, IIRC Jorge, um dos pioneiros da Wikipédia lusófona, teve um esforço imenso em criar as Freguesias e Municípios de Portugal, em pequenos, sucintos, artigos com tanto português quanto se poderia gerar a partir de alguns dados do INE. O problema é que os anos iriam passar, e não haveria forma de actualizar esta informação a não ser fazendo-o manualmente um a um, porque entretanto alguém mudaria o formato do português. Mais tarde, no projecto da criação dos municípios brasileiros, orientado IIRC pelo E2m, alguém se terá apercebido desta dificuldade, e surgiram então os artigos com horríveis marcações (exemplo), provavelmente para alimentar bots que fariam parsing dos dados e fariam a substituição. Mas neste caso, como alguém barafustou meses mais tarde, a edição tornava-se terrível especialmente para os novatos, que se a medo editavam, então quando viam aquelas marcações fugiam!

Demorar-me-ia apenas 6 meses a aprender a trabalhar com bots e a perceber a utilidade das predefinições - a tal ponto que era conhecido pelo maluquinho das predefinições [desculpem não facultar referências, mas teria que procurá-las nos primórdios dos meus milhares de edições...] - para convencer-me que "já que perdemos tempo a fazer isto, faça-mo-lo de forma estruturada, aproximando-nos da linguagem das máquinas, sem prejuízo para a edição, e lancei-me no esforço de fazer isso mesmo: ressuscitando as freguesias e municípios com dados estrutrados.

Terminada esta tarefa, foi altura de iniciar a criação de artigos com base na informação estruturada, mantendo-a siponível (na verdade, houve séries de artigos que foram mesmo feitos com predefinições e, com uma passagem final, foram instanciados com subst:). Mas a informação estruturada iria agora manter-se, e mesmo que não constasse no texto corrido, seria sempre acessível (e facilmente actualizável) nos quadros informativos - basta correr um bot com um simples search & replace por dados actualizados.

Creio que hoje, quiçá por estar mais normalizado em termos de estética (o pessoal, sem querer, foi-se habituando a estes quadros informativos) do que pelos benefícios tecnológicos, já poucos ousam fazer qualquer artigo deste género (do género que se baseia em dados estruturados para constituir informação) sem uma predefinição: temos as Cidades, os Animais (sempre difíceis devido às várias formas de classificação, mas enfim..), os Asteróides, etc.

Mas isto porquê? Porque hoje descobri um projecto interessantíssimo: a DBpedia que, segundo a visão do Tim Berners-Lee, o autor da World Wide Web, é o primeiro passo para aquilo que ele chama de Linked Data: chegámos a um ponto em que as interrelações de informação estão mais do que estabelecidas - mas e as interrelações de dados? O engraçado é que somos vários a pensar assim: OK, uma página web tem, de facto, informação, mas como é que podemos usá-la fora do contexto dessa página - e em grandes quantidades? Será que esses dados - e o esforço de publicá-los - estão condenados a serem só aquilo: inúteis para terceiros? É que extrair informação de páginas de múltiplas fontes não-estruturadas é virtualmente impossível (pode bastar mudar uma vírgula ou uma cor de texto para que o parsing falhe) e obrigar cada pessoa que deseje usar a informação a ter que construir mecanismos que extraia essa informação parece-me um gigantesco desperdício de recursos.. aliás, uma das aplicações que se projectava para o XML/XSL é que ele substituísse o HTML mais tarde ou mais cedo, mas parece que isso nunca vai acontecer.

Então o que Tim Berners-Lee propõe é que a disseminação da informação seja complementada com os dados em bruto que a gerou - ou disponibilizada de forma a que estes possam ser reutilizáveis. E isto é particularmente importante num momento em que há imensas comunidades a gerar conteúdo - é curioso como do trabalho humano passámos para o PC e evoluímos para arquitecturas distribuídas e de escala, e destas evoluímos para plataformas distribuídas em que o factor humano pode ser também (novamente) gerador de substância a uma escala muito, muito maior... mas isto é outro post, noutro dia..

Deixo-vos este artigo interessante sobre a Web semântica, onde se expõem várias formas de relacionamento de dados que se podem obter da web, de forma semântica, e como eles estão (ou podem vir) a ser utilizados:

Vale a pena ver, especialmente para quem, como eu, acha que vivemos numa era dos diabos em que tudo pode acontecer, inclusivé uma

Web [in which computers] become capable of analyzing all the data on the Web

Tim Berners-Lee, 1999

AddThis Social Bookmark Button

Google Translator Toolkit

August 7th, 2009 ntavares Posted in programming, pt_PT, wikipedia No Comments »

Ouvir com webReader

Traduzir artigos de outras Wikipédias para a Wikipédia da Língua Portuguesa é uma forma corrente de, pelo menos, dar um arranque aos artigos com conteúdo. Tomei agora conhecimento do Google Translator Toolkit que, muito embora proponha traduções simplistas, decerto irá ao encontro de muitos editores: torna possível rever e retocar a tradução — que é como sabemos — em dual view, para além de integrar um dicionário de acesso rápido.

Mas o que é fantástico é que a Google propõe aproveitar as correcções para melhorar o seu próprio motor de tradução. Mais um brilhante exemplo de como as comunidades podem gerar mais-valias para os projectos, ao contrário da visão tradicional. Aqui fica o vídeo de demonstração.

Ainda não testei, mas está na calha :)

AddThis Social Bookmark Button

Benchmarking de parâmetros InnoDB

July 15th, 2009 ntavares Posted in mysql, performance, pt_PT No Comments »

Ouvir com webReader

Aqui vão uns benchmarks que fiz uma vez com InnoDB para uma tabela quase exclusivamente de escrita. Com quase exclusivamente quero dizer que 99.99% das operações eram INSERTs. O filesystem, desta vez, era ZFS, e era o mesmo para os logs e datafiles.

Vamos aos testes. Especificações de hardware e SO:

  • MySQL: 5.1.25-rc-log conforme consta no CoolStack
  • Intel Quad core x 2800 MHz
  • 16 GB RAM
  • Solaris 10

As configurações de base eram estas:

CODE:
  1. transaction-isolation = READ-COMMITTED
  2. innodb_file_per_table
  3. innodb_buffer_pool_size = 3000M
  4. innodb_additional_mem_pool_size = 20M
  5. innodb_log_file_size = 256M
  6. innodb_autoinc_lock_mode = 2
  7. innodb_flush_log_at_trx_commit = 2
  8. innodb_file_per_table
  9. log-slow-queries=mysql-query-slow.log
  10. slow_query_log = 1
  11. long_query_time = 1
  12. innodb_doublewrite = 0

E todos os testes foram intercalados de DROP TABLE, CREATE TABLE e uma única alteração à configuração de base. Os INSERT's foram feitos com INSERT .... ON DUPLICATE KEY. O schema anda algures perdido aqui nos meus apontamentos (vou tentar encontrá-lo entranto) mas é de notar que havia um único índice, que era um UNIQUE KEY, e que foi transformado inicialmente para PK e deixado em alguns dos testes seguintes.

A tabela seguinte diz respeito ao único parâmetro que foi alterado em relação à base:

CODE:
  1. Serie A default
  2. Serie B UNIQUE -> PRIMARY KEY
  3. Serie C PK, Autocommit = 0 (1 comm/seg)
  4. Serie D PK, innodb_flush_method = O_DIRECT
  5. Serie F PK, innodb_flush_method = O_DSYNC
  6. Serie G innodb_flush_log_at_trx_commit = 0
  7. Serie H innodb_doublewrite = 0
  8. Serie I innodb_log_file_size = 128M
  9. Serie J zfs set recordsize=16k data/mysql

innodb-parameters-1

Na Série B a única alteração foi a conversão da UNIQUE KEY para uma PRIMARY KEY, nada a assinalar. Na Série C, a alteração consistia em suster os COMMIT's durante aprox. 1 segundo, fazendo COMMIT a cada segundo. Mal seria se não tivéssemos um ganho, por mínimo que fosse, mas é preciso ter em conta que, apesar do ganho ser de 20%, são ~7600 potenciais candidatos a rollback caso alguma coisa corra mal nesse segundo..! Posso dizer que o rollback de aprox. metade demora bastante (para o que é um arranque normal do MySQL).

Queria também experimentar Direct I/O com InnoDB na Série D mesmo sabendo que estava em ZFS, e que a coisa não deveria ser tão fácil. O erro foi:

CODE:
  1. 090721 18:18:10  InnoDB: Failed to set DIRECTIO_ON on file /opt/coolstack/mysql/data/ibdata1: OPEN: Inappropriate ioctl for device, continuing anyway

Na série F, foi a vez de experimentar O_DSYNC, e nem acabei de terminar o teste, pois iria demorar demasiado tempo. Escusado será dizer que o I/O foi altíssimo durante esse momento. Comecei entretanto a procurar maneiras de afinar o ZFS, mas convenhamos que ao fim de uns minutos a ler, já estava a enveredar por um caminho muito muito distante.. :-) É incrível a quantidade de coisas que dá para fazer com ZFS e só por si vai merecer uma categoria própria, um dia...

De volta ao MySQL, a Série G inflinge um risco conhecido, desleixando o flush dos logs, por isso não é de estranhar o aumento de performance - esta opção deve ser analisada com cuidado, pois tem contrapartidas. A Série H desactivou o doublewrite do motor, resultando num aumento de performance de 3,5% (relativamente à Série A) conforme previsto pelo Peter Zaitsev. Como estamos perante ZFS, não vejo motivo nenhum para não desactivá-lo.

Reduzindo o tamanho dos logs na Série I resultou num aumento de performance de 2,8%. Isto é interessante, e é a prova viva de que, se por um lado precisamos deles grandes, por outro, a sua gestão pode infligir mais carga ou deteriorar a performance por serem grandes demais, isto para não falar na forma como afectam o recovery. Não cheguei a testar, mas com um tamanho ainda mais pequeno, o aumento podia ser maior...

A Série J foi dedicada ao ZFS, com a recomendação típica para filesystems dedicados a DBs, que é o alinhamento do tamanho dos blocos com o tamanho das páginas de InnoDB (16KB), e o ZFS, como [quase?] todos os sistemas de ficheiros, permite ajustar esse parâmetro. Traduzindo, a cada leitura do disco, o SO pede ao disco record size (omissão: 128KB) bytes de cada vez; o InnoDB, que tenta gerir os acessos I/O de forma inteligente minimizando-os ao máximo, faz pedidos de page size bytes de cada vez. Se o SO não estiver alinhado, e como os blocos pedidos pelo InnoDB são na maioria aleatórios, cada bloco de 16K solicitado pelo InnoDB traduzir-se-á em leituras de 128KB. Com um pedido de 10 páginas aleatórias (160KB), o SO terá de ler 1280KM, ou seja, quase 10x mais! Mas como eu estava à espera, o resultado não foi significativo (1%) já que este cenário era exclusivamente de INSERTs.

Em termos de sistemas de ficheiros, sejam ZFS ou outro qualquer, há ainda características determinantes a considerar, que são o efeito de prefetching e read-ahead.... mas lá está, já me estava a desviar demasiado do MySQL :P Aqui ficam algumas considerações interessantes sobre o uso de MySQL em ZFS.

Ficaram a faltar muitas opções por falta de tempo, mas aqui ficam sugestões para a quem quiser experimentar:

AddThis Social Bookmark Button