O MySQL não tem função para contar o número de campos não NULL em uma linha, até onde eu sei.
Então, a única maneira que consigo pensar é usar uma condição explícita:
SELECT * FROM mytable
ORDER BY (IF( column1 IS NULL, 0, 1)
+IF( column2 IS NULL, 0, 1)
...
+IF( column45 IS NULL, 0, 1)) DESC;
...é feio como o pecado, mas deve funcionar.
Você também pode criar um TRIGGER para incrementar uma coluna extra "fields_filled". O gatilho custa você em
UPDATE
, os 45 IFs prejudicam você em SELECT
; você terá que modelar o que for mais conveniente. Observe que indexar todos os campos para acelerar
SELECT
vai custar-lhe ao atualizar (e 45 índices diferentes provavelmente custam tanto quanto uma varredura de tabela na seleção, para não dizer que o campo indexado é um VARCHAR
). Execute alguns testes, mas acredito que a solução 45-IF provavelmente será a melhor em geral. ATUALIZAÇÃO :Se você pode retrabalhar sua estrutura de tabela para normalizá-la um pouco, você pode colocar os campos em um
my_values
tabela. Então você teria uma "tabela de cabeçalho" (talvez com apenas um ID exclusivo) e uma "tabela de dados". Campos vazios não existiriam, e então você poderia classificar por quantos campos preenchidos existem usando um RIGHT JOIN
, contando os campos preenchidos com COUNT()
. Isso também aceleraria muito UPDATE
operações e permitiria que você empregasse índices com eficiência. EXEMPLO (da configuração da tabela à configuração de duas tabelas normalizadas) :
Digamos que temos um conjunto de
Customer
registros. Teremos um pequeno subconjunto de dados "obrigatórios" como ID, nome de usuário, senha, e-mail, etc.; então teremos um subconjunto talvez muito maior de dados "opcionais", como apelido, avatar, data de nascimento e assim por diante. Como primeiro passo, vamos supor que todos esses dados sejam varchar
(isso, à primeira vista, parece uma limitação quando comparado à solução de tabela única, onde cada coluna pode ter seu próprio tipo de dados). Então temos uma tabela como,
ID username ....
1 jdoe etc.
2 jqaverage etc.
3 jkilroy etc.
Então temos a tabela de dados opcionais. Aqui John Doe preencheu todos os campos, Joe Q. Média apenas dois, e Kilroy nenhum (mesmo que ele fosse aqui).
userid var val
1 name John
1 born Stratford-upon-Avon
1 when 11-07-1974
2 name Joe Quentin
2 when 09-04-1962
Para reproduzir a saída "single table" no MySQL, temos que criar uma
VIEW
bastante complexa com muitos LEFT JOIN
s. Esta visualização será muito rápida se tivermos um índice baseado em (userid, var)
(melhor ainda se usarmos uma constante numérica ou um SET em vez de um varchar para o tipo de dados de var
:CREATE OR REPLACE VIEW usertable AS SELECT users.*,
names.val AS name // (1)
FROM users
LEFT JOIN userdata AS names ON ( users.id = names.id AND names.var = 'name') // (2)
;
Cada campo em nosso modelo lógico, por exemplo, "name", estará contido em uma tupla ( id, 'name', value ) na tabela de dados opcional.
E ele produzirá uma linha no formato
<FIELDNAME>s.val AS <FIELDNAME>
na seção (1) da consulta acima, referindo-se a uma linha do formulário LEFT JOIN userdata AS <FIELDNAME>s ON ( users.id = <FIELDNAME>s.id AND <FIELDNAME>s.var = '<FIELDNAME>')
na seção (2). Assim, podemos construir a consulta dinamicamente concatenando a primeira linha de texto da consulta acima com uma Seção 1 dinâmica, o texto 'FROM users' e uma Seção 2 construída dinamicamente. Uma vez que fazemos isso, os SELECTs na visão são exatamente idênticos a antes -- mas agora eles buscam dados de duas tabelas normalizadas por meio de JOINs.
EXPLAIN SELECT * FROM usertable;
nos dirá que adicionar colunas a essa configuração não diminui consideravelmente as operações, ou seja, essa solução escala razoavelmente bem.
Os INSERTs terão que ser modificados (só inserimos dados obrigatórios, e apenas na primeira tabela) e UPDATEs também:ou UPDATE a tabela de dados obrigatórios, ou uma única linha da tabela de dados opcional. Mas se a linha de destino não estiver lá, ela deverá ser INSERTed.
Então temos que substituir
UPDATE usertable SET name = 'John Doe', born = 'New York' WHERE id = 1;
com um 'upsert', neste caso
INSERT INTO userdata VALUES
( 1, 'name', 'John Doe' ),
( 1, 'born', 'New York' )
ON DUPLICATE KEY UPDATE val = VALUES(val);
(Precisamos de um
UNIQUE INDEX on userdata(id, var)
para ON DUPLICATE KEY
trabalhar). Dependendo do tamanho da linha e dos problemas de disco, essa alteração pode gerar um ganho de desempenho apreciável.
Observe que, se essa modificação não for realizada, as consultas existentes não gerarão erros - elas falharão silenciosamente .
Aqui, por exemplo, modificamos os nomes de dois usuários; um tem um nome registrado, o outro tem NULL. O primeiro é modificado, o segundo não.
mysql> SELECT * FROM usertable;
+------+-----------+-------------+------+------+
| id | username | name | born | age |
+------+-----------+-------------+------+------+
| 1 | jdoe | John Doe | NULL | NULL |
| 2 | jqaverage | NULL | NULL | NULL |
| 3 | jtkilroy | NULL | NULL | NULL |
+------+-----------+-------------+------+------+
3 rows in set (0.00 sec)
mysql> UPDATE usertable SET name = 'John Doe II' WHERE username = 'jdoe';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> UPDATE usertable SET name = 'James T. Kilroy' WHERE username = 'jtkilroy';
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0
mysql> select * from usertable;
+------+-----------+-------------+------+------+
| id | username | name | born | age |
+------+-----------+-------------+------+------+
| 1 | jdoe | John Doe II | NULL | NULL |
| 2 | jqaverage | NULL | NULL | NULL |
| 3 | jtkilroy | NULL | NULL | NULL |
+------+-----------+-------------+------+------+
3 rows in set (0.00 sec)
Para saber a classificação de cada linha, para os usuários que possuem uma classificação, simplesmente recuperamos a contagem de linhas userdata por id:
SELECT id, COUNT(*) AS rank FROM userdata GROUP BY id
Agora, para extrair linhas na ordem "status preenchido", fazemos:
SELECT usertable.* FROM usertable
LEFT JOIN ( SELECT id, COUNT(*) AS rank FROM userdata GROUP BY id ) AS ranking
ON (usertable.id = ranking.id)
ORDER BY rank DESC, id;
O
LEFT JOIN
garante que indivíduos sem classificação também sejam recuperados, e a ordenação adicional por id
garante que pessoas com classificação idêntica sempre saiam na mesma ordem.