In most cases, for sorting order [not groups], you’ll just use your “up” axis for sorting [that axis of the vert positions], and be OK with some overdraw, majority of the time it’s not worth worrying about given GPU power these days.
For sorting groups – just “group” your sorting order (‘up’ axis vertex value) – this is easier in some cases than others, but generally you’ll do something like “round” each unit [aka sorting group] to nearest integer value, and then the sprites within that group you’ll position, relative to each other, via moving them +/- 0.1f from your up-axis.
Depending on how many sprites & how much precision you need, this could start to get expensive on the CPU side, but most cases you don’t need that level of precision and won’t need to do actual ranking/sorting & can just “let it fly” as-is. If you did need precision & you ran into CPU issues, you could conceivably offload that to a compute shader – but we’ve never had to go down that path & we have production games that render seriously-large numbers of sprites at once and not run into any issues.
Another way to do sorting groups, and what we use in production for one of our games (specifically for characters/agents, which are made up from ~6 sprites each in that title), is:
In your shader have N [ie 6] Texture [or TextureArray] Samplers. Each draw call can then be drawing N ‘sprites’ onto the same quad – simply by sampling all 6 textures at once and combining the result in the shader. This works well for us, but in most cases this means you’re [ab]using things a bit [transparency / discard], and you will see some GPU overhead from the overdraw – in our case, it was the fastest way to go to get sorting groups, though.
Concrete example, our “character” shader from the game above, it has 6 samplers along these lines: T2DArray_Head, T2DArray_Body, T2DArray_Arms, T2DArray_Legs, T2DArray_Torso, T2DArray_Mouth
The result fragment is then (alter to your needs):
final_color += T2DArray_Head.rgb * T2DArray_Head.a;
final_color += T2DArray_Arms.rgb * T2DArray_Arms.a;
final_color += T2DArray_Legs.rgb * T2DArray_Legs.a;
…etc…
final_color.rgb /= 6.0;
You can customize that to behave however you want, so that your shader is combining the colors to your liking (ie deciding which ‘sprite’ takes precedence over which, etc). This obviously works best when you’re rendering lots of a single “type” of like-things. Though if you’re only rendering 300 of something, you’re reading the wrong thread anyways/optimizing too early. 
FWIW, not saying these are the only ways or even the best ways – just ways that I know will work and that are capable of production-caliber results, based on our experience shipping games in production environments. 
Hope that helps!