code: func (e *HashAggExec) unparallelExec(ctx context.Context, chk *chunk.Chunk) error {
  	// In this stage we consider all data from src as a single group.
  	if !e.prepared {
  		err := e.execute(ctx)
  		if err != nil {
  			return err
  		}
  		if (len(e.groupSet) == 0) && len(e.GroupByItems) == 0 {
  			// If no groupby and no data, we should add an empty group.
  			// For example:
  			// "select count(c) from t;" should return one row [0]
  			// "select count(c) from t group by c1;" should return empty result set.
  			e.groupSet.Insert("")
  			e.groupKeys = append(e.groupKeys, "")
  		}
  		e.prepared = true
  	}
  	chk.Reset()
  
  	// Since we return e.maxChunkSize rows every time, so we should not traverse
  	// `groupSet` because of its randomness.
  	for ; e.cursor4GroupKey < len(e.groupKeys); e.cursor4GroupKey++ {
  		partialResults := e.getPartialResults(e.groupKeys[e.cursor4GroupKey])
  		if len(e.PartialAggFuncs) == 0 {
  			chk.SetNumVirtualRows(chk.NumRows() + 1)
  		}
  		for i, af := range e.PartialAggFuncs {
  			if err := af.AppendFinalResult2Chunk(e.ctx, partialResults[i], chk); err != nil {
  				return err
  			}
  		}
  		if chk.IsFull() {
  			e.cursor4GroupKey++
  			return nil
  		}
  	}
  	return nil
  }
docs: // unparallelExec executes hash aggregation algorithm in single thread.