Group by no Couchdb

No post anterior sobre Order by no CouchDB eu mencionei que o próximo post seria sobre Group by, como prometido vou tentar abordar essa questão apresentando um exemplo simples de como agrupar resultados de views.

Para facilitar (como sempre) estou utilizando Ruby on Rails + CouchRest, porém vou postar aqui só a parte relevante do código para entendermos o exemplo. Então vamos considerar nosso model Post:

class Post < CouchRest::ExtendedDocument
  use_database CouchRest.database("http://127.0.0.1:5984/group_couchdb")

  property :titulo
  property :conteudo
  property :autor_id

  timestamps!

  view_by :data,
  :map => "
    function(doc) {
      if ((doc['couchrest-type'] == 'Post')) {
        data = doc.created_at;
        ano = parseInt(data.substr(0, 4));
        mes = parseInt(data.substr(5, 2), 10);
        dia = parseInt(data.substr(8, 2), 10);

        emit([ano, mes, dia], 1);
      }
    }",
  :reduce => "_count"

end

Detalhe para a view data que emite como chave um array contendo o ano, mes e dia do post e para cada valor (dependendo do agrupamento) emite o valor 1 para a função reduce (que conta quantos valores foram emitidos para cada chave em questão).

Não vou postar o código de inserção dos posts, você pode inserir alguns posts na sua base para realizar os testes.
Primeiro quero contar o número de posts realizados por dia (isso é possível pois a função map gera o array com dia, mês e ano, assim a função reduce soma os valores para as chaves).

curl http://127.0.0.1:5984/group_couchdb/_design/Post/_view/by_data?group=true

Acessando a url acima você vai ver um resultado parecido com esse:

{"rows":[
  {"key":[2010,6,10],"value":1},
  {"key":[2010,7,17],"value":1},
  {"key":[2010,7,18],"value":2}
]}

Ou seja, no dia 10/06/2010 foi cadastrado um post, assim como no dia 17/07/2010, já no dia 18/07/2010 foram cadastrados dois posts (esses são os posts que eu tenho cadastrado na minha base).

Agora vamos supor que queremos contar quantos posts foram cadastrados por ano. Isso é muito simples utilizando a view que já temos, o CouchDB aceita um parâmetro chamado group_level. Cada posição do array que foi emitida é um “level”, então podemos agrupar os valores por cada posição do array. Se passarmos group_level=2 serão emitidos como chave o ano e o mês, group_level=1 apenas o mês, etc.

Post.by_data(:reduce => true, :group_level=>1)

A url seria parecida com essa:


http://127.0.0.1:5984/group_couchdb/_design/Post/_view/by_data?group=true&group_level=1

E o resultado seria quatro posts para o ano de 2010

{"rows":[
  {"key":[2010],"value":4}
]}

Podemos concluir que agrupar valores no CouchDB não é tão difícil quanto parece, acredito que é uma questão da modelagem dos dados e da forma como utilizamos as views.

Order by no CouchDB

Uma das cláusulas mais utilizadas em SQL nos bancos relacionais é a ORDER BY. Sempre que queremos ordenar o resultado de uma query por determinada coluna de uma tabela, utilizamos ORDER BY nome_da_coluna e recomenda-se que seja criado um índice para essa coluna, afim de otimizar a ordenação dos dados. O problema começa quando precisamos ordernar os resultados por vários campos da tabela, sendo que pode ficar inviável criar uma série de índices, já que essa tabela pode também sofrer um grande número de atualizações.

Uma das grandes vantagens do CouchDB (noSQL) é o poder das views (eu sempre gosto de reforçar isso). Você pode criar várias views (que resultem em várias formas) para os seus dados, sem comprometer a inserção para aquele tipo de dado (documento). Ou seja, supondo que você precise ordenar alguns documentos por 3 “campos” diferentes, basta que você crie 3 views, sendo que cada uma emita como chave o determinado “campo” em questão. Podemos exemplificar em Ruby utilizando a gem CouchRest, vamos supor um modelo de cidades:

class City < CouchRest::ExtendedDocument
  use_database CouchRest.database("http://127.0.0.1:5984/order_by_couchdb")
  unique_id :slug
  
  property :slug, :read_only => true
  property :name
  property :state
  property :population
  timestamps!
  
  #o couchrest criara as views no banco
  view_by :name
  view_by :state
  view_by :population
  
  #antes de salvar cria o slug que sera o id do documento
  set_callback :save, :before, :generate_slug_from_name

  def generate_slug_from_name
    self['slug'] = name.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new?
  end
end

Vamos inserir algumas cidades para exemplificar (eu estou utilizando Rails também)

c = City.new(:name => "Sorocaba", :state => "SP", :population => 700000)
c.save

c2 = City.new(:name => "Sao Paulo", :state => "SP", :population => 11000000)
c2.save

c3 = City.new(:name => "Rio de Janeiro", :state => "RJ", :population => 6000000)
c3.save

O CouchRest vai criar 3 views (by_name, by_state, by_population). Ordenando as cidades pelo nome

City.by_name

Repare que no banco será criada a view (função map)

function(doc) {
  if ((doc['couchrest-type'] == 'City') && doc['name']) {
    emit(doc['name'], null);
  }
}

E o resultado será (ordenado alfabeticamente)

{"total_rows":3,"offset":0,"rows":[
  {"id":"rio-de-janeiro","key":"Rio de Janeiro","value":null},
  {"id":"sao-paulo","key":"Sao Paulo","value":null},
  {"id":"sorocaba","key":"Sorocaba","value":null}
]}

Ordenando as cidades pelo número da população, da maior para a menor

City.by_population(:descending => true)
{"total_rows":3,"offset":0,"rows":[
  {"id":"sao-paulo","key":11000000,"value":null},
  {"id":"rio-de-janeiro","key":6000000,"value":null},
  {"id":"sorocaba","key":600000,"value":null}
]}

Até aí tudo muito simples e fácil, podemos ordenar nossos dados de forma bem flexível. Então, vamos complicar um pouco. Vamos supor que queremos filtrar os resultados passando como parâmetro o estado, porém que as cidades sejam ordenadas pela data de criação, de forma que as mais velhas sejam mostradas primeiro. A princípio isso parece estranho, mas é muito útil por exemplo se quisermos ordernar posts, buscando por determinada categoria (queremos que sejam listados os últimos cadastrados) e também em outras situações que se precisa ordenar os resultados de forma crescente ou decrescente.

Se utilizarmos a view que emite apenas o estado como chave (by_state), o CouchDB vai ordenar as cidades pelo id (no caso o id é o nome da cidade) então teríamos as cidades do determinado estado, ordenadas alfabeticamente. Então, precisamos criar uma view que emita um array (estado e data de criação) como chave.
Acrescente na classe

view_by :state, :created_at

Será criada a seguinte view

function(doc) {
  if ((doc['couchrest-type'] == 'City') && doc['state'] && doc['created_at']) {
    emit([doc['state'], doc['created_at']], null);
  }
}

Buscando as cidades (cadastradas primeiro) do estado SP

City.by_state_and_created_at(:startkey => ['SP'], :endkey => ['SP', {}])

Repare que a cidade de Sorocaba vem primeiro (pois foi cadastrada primeiro), se tivéssemos utilizado a view que emite como chave apenas o estado, São Paulo viria primeiro

{"total_rows":3,"offset":1,"rows":[
  {"id":"sorocaba","key":["SP","2010/05/25 01:25:53 +0000"],"value":null},
  {"id":"sao-paulo","key":["SP","2010/05/25 01:25:54 +0000"],"value":null}
]}

Podemos concluir que as views do CouchDB são extremamente poderosas e simples, de início parece complicado, já que estamos habituados ao SQL, porém quando se entende bem o conceito do Map/Reduce as coisas ficam bem mais fáceis. No próximo post pretendo escrever sobre agrupamentos (GROUP BY).

CouchRest ExtendedDocument – Queries Básicas

Em ruby temos algumas gems para serem utilizadas com o CouchDB. Particularmente eu prefiro a gem CouchRest, construída pelos commiters do CouchDB (Chris Anderson é um deles). E um dos commiters do CouchRest (que vem fazendo um excelente trabalho) é o Marcos Tapajos da Improve IT e RedeParede.
Para saber mais como instalar e utilizar o CouchRest acesse a wiki no Github.

Vamos considerar a seguinte classe (Post) que herda ExtendedDocument da gem CouchRest

class Post < CouchRest::ExtendedDocument
  use_database DB #constante com as infos do seu banco

  property :titulo
  property :conteudo

  #será criado no banco uma view pelo título
  view_by :titulo

end

Carregando todos os documentos com o “couchrest-type” Post

@posts = Post.all
puts @posts.inspect

Carregando um documento pelo ID

#Sem excessões caso o documento não seja encontrado
@post = Post.get("id_do_documento")
puts @post.inspect

#Será gerado uma excessão se o documento não for encontrado
@post = Post.get!("id_do_documento")
puts @post.inspect

Utilizando Views

A view criada pelo CouchRest

function(doc) {
  if ((doc['couchrest-type'] == 'Post') && doc['titulo']) {
    emit(doc['titulo'], null);
  }
}
#Carregando todos do documentos com "couchrest-type" Post e que tenham um título
@posts = Post.by_titulo
puts "O número de posts encontrados foi #{@posts.count}"
puts
puts @posts.inspect

#Carregando todos do documentos com "couchrest-type" Post e um título específico
@posts = Post.by_titulo(:key => "Um título em específico")
puts @posts.inspect

CouchDB Views – exemplo prático

O CouchDB possui uma interface web (melhor que um phpmyadmin por exemplo) para administração dos dados, chamada Futon. Para acessá-la abra em seu browser:

http://localhost:5984/_utils/

Vamos iniciar com um exemplo bem simples de como utilizar views.
As views são usadas para recuperar os dados do banco. De início talvez pareça um pouco complicado, a maioria de nós está completamente adaptada ao SQL, mas aos poucos vamos mudando nossa forma de pensar (até mesmo quebrando a forma “relacional” de armazenamento dos dados) e vamos aprendendo conceitos interessantes, como Map/Reduce.

Vamos supor que precisamos armazenar dados de estudantes (nome e idade apenas). Nos bancos relacionais teríamos que criar uma tabela “estudantes” com os devidos campos e tipos. Isso pode não ser tão bom, já que nem todos os estudantes podem ter as mesmas informações (representação dos dados). Os documentos são bem melhores para representarmos os elementos da vida real, do que as tabelas, são extremamente flexíveis. No CouchDB o documento (JSON) ficaria mais ou menos assim:

Um detalhe para o atributo type (que eu criei) para “informar” qual o tipo de documento, no caso se refere a estudantes.

Vamos supor que teríamos outros documentos no banco (de outros “types” também) e que gostaríamos de selecionar apenas as informações de estudantes. Em SQL seria:

SELECT * FROM students

No CouchDB poderíamos criar uma view (em javascript) assim:

function(doc) {
  if(doc['type'] == 'Student') {
    emit(doc['name'], doc);
  }
}

Detalhe que essa é uma função do tipo Map. Serão selecionados apenas os documentos com type igual a “Student” e a chave é o nome do estudante.

Existe uma série de vantagens em utilizar o conceito de Map/Reduce, principalmente quando se trata de aplicações que rodam em clusters.
Você também pode ler como funcionam os índices de views no Couch.

View Indexes

Antes de começar com a tradução sobre os índices de views do CouchDB, quero fazer um agradecimento especial ao Marcos Tapajós pela divulgação em CouchDB para brasileiros. O Tapajós trabalha para o Redeparede, para a Improve It e é commiter da gem CouchRest. Valeu Tapajós =]

Índices de views

As views são representações dinâmicas do conteúdo atual dos documentos de um banco de dados, e o CouchDB faz ser simples a criação de views úteis dos dados. Mas gerar uma view de um banco de dados com centenas de milhares ou milhões de documentos leva tempo e consome recursos, não é algo que o sistema deve fazer do zero toda hora.

Para manter a exibição de consultas rápida, a view engine mantém índices dessas views, e atualiza-os incrementalmente para refletir as mudanças no banco de dados. O design do core do CouchDB é largamente otimizado para criação eficiente de views e seus índices.

Visões e suas funções são definidas dentro de documentos especiais de “design” e um documento de design pode conter várias funções de views (unicamente nomeadas). Quando um usuário abre uma view e seu índice é automaticamente atualizado, todas as views no mesmo documento de design são indexadas em um único grupo.

O construtor de views usa o ID de sequência do banco de dados para determinar se um grupo de views está totalmente atualizado com o banco de dados. Se não, a view engine examina todos os documentos do banco (embalados em ordem sequencial) mudados desde a última atualização. Os documentos são lidos na ordem em que estão os arquivos no disco, reduzindo a frequência e custo de buscas no disco.

As views podem ser lidas e consultadas simultaneamente enquanto estão também sendo atualizadas. Se um cliente está recebendo o conteúdo de uma view grande, a mesma view pode ser aberta e atualizada concorrentemente por outro cliente sem bloquear o primeiro cliente. Isso é verdade para qualquer número de leituras de clientes simultâneos, que podem ler e consultar a view enquanto o índice está concorrentemente sendo atualizado por outros clientes sem causar problemas para os leitores.

Assim os documentos são examinados, os seus valores de linha anterior são removidos dos índices de views, se existirem. Se o documento for selecionado por uma função de view, os resultados da função são inseridos na view como uma nova linha.

Quando as mudanças nos índices de views são escritas no disco, as atualizações são sempre anexadas ao fim do arquivo, servindo tanto para reduzir as buscas no disco durante commits e para garantir que crashes e falhas de falta de energia não corrompam os índices. Se uma falha ocorrer durante uma atualização de um índice, a atualização incompleta do índice é simplesmente perdida e reconstruída incrementalmente a partir do seu estado anterior.

Essa é uma tradução adaptada:
http://couchdb.apache.org/docs/overview.html

Views no CouchDB

Views são o método de agregação e relatórios sobre os documentos em uma base de dados (os SELECTs dos bancos relacionais), e são construídas por demanda para agregar, juntar e reportar documentos. As views são construídas dinamicamente e não afetam o documento subjacente, você poder ter várias representações diferentes dos mesmos dados que desejar.

As definições de views são estritamente virtuais e somente mostram os documentos da instancia corrente do banco de dados, fazendo-os separados dos dados que eles mostram e compatíveis com replicação. As views do CouchDB são definidas dentro de documentos de design especiais e podem ser replicadas em várias instancias de bancos de dados como documentos regulares, de modo que não apenas os dados são replicados no CouchDB, mas o design da aplicação inteira também é replicado.

Funções Javascript

As views são definidas usando funções javascript que atuam como map em um sistema de map-reduce. Uma view pega um documento do CouchDB como um argumento e então faz o cálculo que precisar fazer para determinar os dados que serão disponibilizados pela view, se houverem. É possível adicionar multiplas linhas para a view com base em um único documento.

Índices de Views

Essa é uma tradução adaptada:
http://couchdb.apache.org/docs/overview.html