Como selecionar uma subconsulta usando o Laravel Query Builder?

102

Eu gostaria de obter valor pelo seguinte SQL usando o Eloquent ORM.

- SQL

 SELECT COUNT(*) FROM 
 (SELECT * FROM abc GROUP BY col1) AS a;

Então eu considerei o seguinte.

- Código

 $sql = Abc::from('abc AS a')->groupBy('col1')->toSql();
 $num = Abc::from(\DB::raw($sql))->count();
 print $num;

Estou procurando uma solução melhor.

Por favor me diga a solução mais simples.

quenty658
fonte

Respostas:

131

Além da resposta de @ delmadord e seus comentários:

Atualmente não há nenhum método para criar uma subconsulta na FROMcláusula, então você precisa usar manualmente a instrução bruta e, se necessário, mesclar todas as ligações:

$sub = Abc::where(..)->groupBy(..); // Eloquent Builder instance

$count = DB::table( DB::raw("({$sub->toSql()}) as sub") )
    ->mergeBindings($sub->getQuery()) // you need to get underlying Query Builder
    ->count();

Lembre-se de que você precisa mesclar as ligações na ordem correta . Se você tiver outras cláusulas vinculadas, deve colocá-las após mergeBindings:

$count = DB::table( DB::raw("({$sub->toSql()}) as sub") )

    // ->where(..) wrong

    ->mergeBindings($sub->getQuery()) // you need to get underlying Query Builder

    // ->where(..) correct

    ->count();
Jarek Tkaczyk
fonte
3
Observe que se você tiver uma consulta complexa como belongsToManygetQuery()$sub->getQuery()->getQuery()
subseleção
1
@Skyzer Você não está lendo o que escrevo. Nada é escapado quando você liga toSql. Leia sobre o PDO php.net/manual/en/book.pdo.php e veja o resultado de seu$query->toSql()
Jarek Tkaczyk
5
Com relação a -> mergeBindings ($ sub-> getQuery ()) basta fazer -> mergeBindings ($ sub)
Jimmy Ilenloa
1
@JimmyIlenloa Se a $subconsulta for um Eloquent Builder , então você ainda precisa da ->getQuery()parte, caso contrário, obterá um erro, uma vez que este método é baseado em tipo contra a Query\Builderclasse.
Jarek Tkaczyk
1
@Kannan nope. é um candidato a RP, eu acho, mas no final não é um caso de uso muito comum. Provavelmente esse é o motivo de não tê-lo até hoje.
Jarek Tkaczyk
76

Laravel v5.6.12 (14/03/2018) adicionado fromSub()e fromRaw()métodos para o construtor de consultas (# 23476) .

A resposta aceita está correta, mas pode ser simplificada em:

DB::query()->fromSub(function ($query) {
    $query->from('abc')->groupBy('col1');
}, 'a')->count();

O snippet acima produz o seguinte SQL:

select count(*) as aggregate from (select * from `abc` group by `col1`) as `a`
mpskovvang
fonte
15

A solução do @JarekTkaczyk é exatamente o que eu procurava. A única coisa que sinto falta é como fazer quando você está usando DB::table()consultas. Nesse caso, é assim que eu faço:

$other = DB::table( DB::raw("({$sub->toSql()}) as sub") )->select(
    'something', 
    DB::raw('sum( qty ) as qty'), 
    'foo', 
    'bar'
);
$other->mergeBindings( $sub );
$other->groupBy('something');
$other->groupBy('foo');
$other->groupBy('bar');
print $other->toSql();
$other->get();

Atenção especial como fazer mergeBindingssem usar o getQuery()método

Thiago Mata
fonte
Usar DB::raw()fez o trabalho para mim
Nino Škopac
7

No laravel 5.5 há um método dedicado para subconsultas e você pode usá-lo assim:

Abc::selectSub(function($q) {
    $q->select('*')->groupBy('col1');
}, 'a')->count('a.*');

ou

Abc::selectSub(Abc::select('*')->groupBy('col1'), 'a')->count('a.*');
Sasa Blagojevic
fonte
1
Parece que subSelect só pode ser usado para adicionar uma subconsulta a SELECT, não FROM.
hagabaka de
1
Call to undefined method subSelect()parece subSelectque não existe.
Maruf Alom
3
Obrigado por trazer isso ao meu conhecimento, eu escrevi errado o nome, deveria ter sido selectSub. Eu atualizei minha resposta agora.
Sasa Blagojevic
3

Eu gosto de fazer algo assim:

Message::select('*')
->from(DB::raw("( SELECT * FROM `messages`
                  WHERE `to_id` = ".Auth::id()." AND `isseen` = 0
                  GROUP BY `from_id` asc) as `sub`"))
->count();

Não é muito elegante, mas é simples.

Guy Mazuz
fonte
Obrigado, isso funcionou para mim, como uma observação lateral, tenha cuidado com o conteúdo selecionado porque laravel adicionou algumas aspas e eu tive que usar -> select (\ DB :: raw ('Your select')) para me livrar delas.
Wak
2

Não consegui fazer seu código para fazer a consulta desejada, o AS é um alias apenas para a tabela abc, não para a tabela derivada. O Laravel Query Builder não suporta implicitamente apelidos de tabela derivados, DB :: raw é provavelmente necessário para isso.

A solução mais direta que encontrei é quase idêntica à sua, mas produz a consulta que você solicitou:

$sql = Abc::groupBy('col1')->toSql();
$count = DB::table(DB::raw("($sql) AS a"))->count();

A consulta produzida é

select count(*) as aggregate from (select * from `abc` group by `col1`) AS a;
peter.babic
fonte
Obrigado por sua resposta. Há um problema no método de "Abc :: from (???) e DB :: table (???)". $ sql = Abc :: where ('id', '=', $ id) -> groupBy ('col1') -> toSql (); $ count = DB :: table (DB :: raw ("($ sql) AS a")) -> count (); Ocorre um erro de SQL no código acima. - onde e atribuir parâmetro!
quenty658
2

A maneira correta descrita nesta resposta: https://stackoverflow.com/a/52772444/2519714 A resposta mais popular no momento atual não é totalmente correta.

Desta forma, https://stackoverflow.com/a/24838367/2519714 não é correto em alguns casos como: sub select has where bindings, a seguir juntando a tabela para sub select, então outro where adicionado a todas as consultas. Por exemplo, consulta: select * from (select * from t1 where col1 = ?) join t2 on col1 = col2 and col3 = ? where t2.col4 = ? Para fazer esta consulta, você escreverá um código como:

$subQuery = DB::query()->from('t1')->where('t1.col1', 'val1');
$query = DB::query()->from(DB::raw('('. $subQuery->toSql() . ') AS subquery'))
    ->mergeBindings($subQuery->getBindings());
$query->join('t2', function(JoinClause $join) {
    $join->on('subquery.col1', 't2.col2');
    $join->where('t2.col3', 'val3');
})->where('t2.col4', 'val4');

Durante a execução dessa consulta, seu método $query->getBindings()retornará ligações em ordem incorreta, como ['val3', 'val1', 'val4']neste caso, em vez de corrigir ['val1', 'val3', 'val4']para sql bruto descrito acima.

Mais uma vez a maneira correta de fazer isso:

$subQuery = DB::query()->from('t1')->where('t1.col1', 'val1');
$query = DB::query()->fromSub($subQuery, 'subquery');
$query->join('t2', function(JoinClause $join) {
    $join->on('subquery.col1', 't2.col2');
    $join->where('t2.col3', 'val3');
})->where('t2.col4', 'val4');

Além disso, as ligações serão automaticamente e corretamente mescladas na nova consulta.

dkop
fonte
Muito obrigado! Ajudou muito!
Hasnat Babur